From 8888bf2d60428e5af77c8892ec4d48bd533403ea Mon Sep 17 00:00:00 2001 From: Awiteb Date: Thu, 18 Jul 2024 15:21:05 +0300 Subject: [PATCH] feat: Chat request implementation In this patch, I have implemented the chat request and response from OTMP protocol. The changes include: - New `in_chat_request` and `out_chat_request` database tables - New `users_status` table to store user status (whitelisted or blacklisted) - New server events: - `ChatRequest` To send chat request comes from some user - `ChatRequestResponse` To send chat request response to some user - New client events: - `ChatRequest` To send chat request to some user - `ChatRequestResponse` To send chat request response to some user - `ws_errors` macro to create websocket errors - `/user/whitelist` and `/user/blacklist` API to list whitelisted and blacklisted users Fixes: https://git.4rs.nl/OxideTalis/oxidetalis/issues/2 Reviewed-on: https://git.4rs.nl/OxideTalis/oxidetalis/pulls/14 Reviewed-by: Amjad Alsharafi Helped-by: Amjad Alsharafi Signed-off-by: Awiteb --- Cargo.lock | 1 + .../src/database/in_chat_requests.rs | 61 ++++ crates/oxidetalis/src/database/mod.rs | 6 + .../src/database/out_chat_requests.rs | 102 ++++++ crates/oxidetalis/src/database/user.rs | 25 +- crates/oxidetalis/src/database/user_status.rs | 306 ++++++++++++++++++ crates/oxidetalis/src/errors.rs | 78 ++--- crates/oxidetalis/src/extensions.rs | 44 ++- crates/oxidetalis/src/macros.rs | 93 ++++++ crates/oxidetalis/src/main.rs | 8 +- crates/oxidetalis/src/parameters/mod.rs | 21 ++ .../oxidetalis/src/parameters/pagination.rs | 131 ++++++++ crates/oxidetalis/src/routes/errors.rs | 77 +++++ crates/oxidetalis/src/routes/mod.rs | 3 + crates/oxidetalis/src/routes/user.rs | 88 ++++- crates/oxidetalis/src/schemas/user.rs | 58 ++++ crates/oxidetalis/src/websocket/errors.rs | 55 ++-- .../oxidetalis/src/websocket/events/client.rs | 10 +- .../oxidetalis/src/websocket/events/server.rs | 28 +- .../src/websocket/handlers/chat_request.rs | 138 ++++++++ .../oxidetalis/src/websocket/handlers/mod.rs | 21 ++ crates/oxidetalis/src/websocket/mod.rs | 59 +++- crates/oxidetalis_entities/Cargo.toml | 3 +- .../src/incoming_chat_requests.rs | 57 ++++ crates/oxidetalis_entities/src/lib.rs | 3 + .../src/outgoing_chat_requests.rs | 57 ++++ crates/oxidetalis_entities/src/prelude.rs | 24 ++ crates/oxidetalis_entities/src/users.rs | 31 +- .../oxidetalis_entities/src/users_status.rs | 66 ++++ .../create_incoming_chat_requests_table.rs | 89 +++++ .../create_outgoing_chat_requests_table.rs | 94 ++++++ .../src/create_users_status.rs | 128 ++++++++ .../src/create_users_table.rs | 4 +- crates/oxidetalis_migrations/src/lib.rs | 13 +- 34 files changed, 1860 insertions(+), 122 deletions(-) create mode 100644 crates/oxidetalis/src/database/in_chat_requests.rs create mode 100644 crates/oxidetalis/src/database/out_chat_requests.rs create mode 100644 crates/oxidetalis/src/database/user_status.rs create mode 100644 crates/oxidetalis/src/macros.rs create mode 100644 crates/oxidetalis/src/parameters/mod.rs create mode 100644 crates/oxidetalis/src/parameters/pagination.rs create mode 100644 crates/oxidetalis/src/routes/errors.rs create mode 100644 crates/oxidetalis/src/websocket/handlers/chat_request.rs create mode 100644 crates/oxidetalis/src/websocket/handlers/mod.rs create mode 100644 crates/oxidetalis_entities/src/incoming_chat_requests.rs create mode 100644 crates/oxidetalis_entities/src/outgoing_chat_requests.rs create mode 100644 crates/oxidetalis_entities/src/users_status.rs create mode 100644 crates/oxidetalis_migrations/src/create_incoming_chat_requests_table.rs create mode 100644 crates/oxidetalis_migrations/src/create_outgoing_chat_requests_table.rs create mode 100644 crates/oxidetalis_migrations/src/create_users_status.rs diff --git a/Cargo.lock b/Cargo.lock index 8c8c1fc..4b1fc6a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1965,6 +1965,7 @@ dependencies = [ name = "oxidetalis_entities" version = "0.1.0" dependencies = [ + "chrono", "sea-orm", ] diff --git a/crates/oxidetalis/src/database/in_chat_requests.rs b/crates/oxidetalis/src/database/in_chat_requests.rs new file mode 100644 index 0000000..0e36c74 --- /dev/null +++ b/crates/oxidetalis/src/database/in_chat_requests.rs @@ -0,0 +1,61 @@ +// OxideTalis Messaging Protocol homeserver implementation +// Copyright (C) 2024 OxideTalis Developers +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +//! Database extension for the `in_chat_requests` table. + +use chrono::Utc; +use oxidetalis_core::types::PublicKey; +use oxidetalis_entities::prelude::*; +use sea_orm::{sea_query::OnConflict, DatabaseConnection}; + +use crate::errors::ServerResult; + +/// Extension trait for the `in_chat_requests` table. +pub trait InChatRequestsExt { + /// Save the chat request in the recipient table + async fn save_in_chat_request( + &self, + requester: &PublicKey, + recipient: &UserModel, + ) -> ServerResult<()>; +} + +impl InChatRequestsExt for DatabaseConnection { + #[logcall::logcall] + async fn save_in_chat_request( + &self, + sender: &PublicKey, + recipient: &UserModel, + ) -> ServerResult<()> { + InChatRequestsEntity::insert(InChatRequestsActiveModel { + recipient_id: Set(recipient.id), + sender: Set(sender.to_string()), + in_on: Set(Utc::now()), + ..Default::default() + }) + .on_conflict( + OnConflict::columns([ + InChatRequestsColumn::RecipientId, + InChatRequestsColumn::Sender, + ]) + .do_nothing() + .to_owned(), + ) + .exec(self) + .await?; + Ok(()) + } +} diff --git a/crates/oxidetalis/src/database/mod.rs b/crates/oxidetalis/src/database/mod.rs index a9578ae..c4011ac 100644 --- a/crates/oxidetalis/src/database/mod.rs +++ b/crates/oxidetalis/src/database/mod.rs @@ -16,6 +16,12 @@ //! Database utilities for the OxideTalis homeserver. +mod in_chat_requests; +mod out_chat_requests; mod user; +mod user_status; +pub use in_chat_requests::*; +pub use out_chat_requests::*; pub use user::*; +pub use user_status::*; diff --git a/crates/oxidetalis/src/database/out_chat_requests.rs b/crates/oxidetalis/src/database/out_chat_requests.rs new file mode 100644 index 0000000..c3a3ea1 --- /dev/null +++ b/crates/oxidetalis/src/database/out_chat_requests.rs @@ -0,0 +1,102 @@ +// OxideTalis Messaging Protocol homeserver implementation +// Copyright (C) 2024 OxideTalis Developers +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +//! Database extension for the `out_chat_requests` table. + +use chrono::Utc; +use oxidetalis_core::types::PublicKey; +use oxidetalis_entities::prelude::*; +use sea_orm::DatabaseConnection; + +use crate::{errors::ServerResult, websocket::errors::WsError}; + +/// Extension trait for the `out_chat_requests` table. +pub trait OutChatRequestsExt { + /// Returns the outgoing chat request if the `user` have a sent chat request + /// to the `recipient` + async fn get_chat_request_to( + &self, + requester: &UserModel, + recipient: &PublicKey, + ) -> ServerResult>; + + /// Save the chat request in the requester table + async fn save_out_chat_request( + &self, + requester: &UserModel, + recipient: &PublicKey, + ) -> ServerResult<()>; + + /// Remove the chat request from requester table + async fn remove_out_chat_request( + &self, + requester: &UserModel, + recipient: &PublicKey, + ) -> ServerResult<()>; +} + +impl OutChatRequestsExt for DatabaseConnection { + #[logcall::logcall] + async fn get_chat_request_to( + &self, + requester: &UserModel, + recipient: &PublicKey, + ) -> ServerResult> { + requester + .find_related(OutChatRequestsEntity) + .filter(OutChatRequestsColumn::Recipient.eq(recipient.to_string())) + .one(self) + .await + .map_err(Into::into) + } + + #[logcall::logcall] + async fn save_out_chat_request( + &self, + requester: &UserModel, + recipient: &PublicKey, + ) -> ServerResult<()> { + if let Err(err) = (OutChatRequestsActiveModel { + sender_id: Set(requester.id), + recipient: Set(recipient.to_string()), + out_on: Set(Utc::now()), + ..Default::default() + } + .save(self) + .await) + { + match err.sql_err() { + Some(SqlErr::UniqueConstraintViolation(_)) => { + return Err(WsError::AlreadySendChatRequest.into()); + } + _ => return Err(err.into()), + } + } + + Ok(()) + } + + async fn remove_out_chat_request( + &self, + requester: &UserModel, + recipient: &PublicKey, + ) -> ServerResult<()> { + if let Some(out_model) = self.get_chat_request_to(requester, recipient).await? { + out_model.delete(self).await?; + } + Ok(()) + } +} diff --git a/crates/oxidetalis/src/database/user.rs b/crates/oxidetalis/src/database/user.rs index 7f52764..4e94901 100644 --- a/crates/oxidetalis/src/database/user.rs +++ b/crates/oxidetalis/src/database/user.rs @@ -21,27 +21,29 @@ use oxidetalis_core::types::PublicKey; use oxidetalis_entities::prelude::*; use sea_orm::DatabaseConnection; -use crate::errors::{ApiError, ApiResult}; +use crate::{errors::ServerResult, routes::ApiError}; pub trait UserTableExt { /// Returns true if there is users in the database - async fn users_exists_in_database(&self) -> ApiResult; + async fn users_exists_in_database(&self) -> ServerResult; /// Register new user - async fn register_user(&self, public_key: &PublicKey, is_admin: bool) -> ApiResult<()>; + async fn register_user(&self, public_key: &PublicKey, is_admin: bool) -> ServerResult<()>; + /// Returns user by its public key + async fn get_user_by_pubk(&self, public_key: &PublicKey) -> ServerResult>; } impl UserTableExt for DatabaseConnection { #[logcall] - async fn users_exists_in_database(&self) -> ApiResult { + async fn users_exists_in_database(&self) -> ServerResult { UserEntity::find() .one(self) .await - .map_err(Into::into) .map(|u| u.is_some()) + .map_err(Into::into) } #[logcall] - async fn register_user(&self, public_key: &PublicKey, is_admin: bool) -> ApiResult<()> { + async fn register_user(&self, public_key: &PublicKey, is_admin: bool) -> ServerResult<()> { if let Err(err) = (UserActiveModel { public_key: Set(public_key.to_string()), is_admin: Set(is_admin), @@ -51,10 +53,19 @@ impl UserTableExt for DatabaseConnection { .await { if let Some(SqlErr::UniqueConstraintViolation(_)) = err.sql_err() { - return Err(ApiError::DuplicatedUser); + return Err(ApiError::AlreadyRegistered.into()); } } Ok(()) } + + #[logcall] + async fn get_user_by_pubk(&self, public_key: &PublicKey) -> ServerResult> { + UserEntity::find() + .filter(UserColumn::PublicKey.eq(public_key.to_string())) + .one(self) + .await + .map_err(Into::into) + } } diff --git a/crates/oxidetalis/src/database/user_status.rs b/crates/oxidetalis/src/database/user_status.rs new file mode 100644 index 0000000..1997332 --- /dev/null +++ b/crates/oxidetalis/src/database/user_status.rs @@ -0,0 +1,306 @@ +// OxideTalis Messaging Protocol homeserver implementation +// Copyright (C) 2024 OxideTalis Developers +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +//! Database extension to work with the whitelist table + +use std::num::{NonZeroU32, NonZeroU8}; + +use chrono::Utc; +use oxidetalis_core::types::PublicKey; +use oxidetalis_entities::prelude::*; +use sea_orm::DatabaseConnection; + +use crate::{errors::ServerResult, websocket::errors::WsError}; + +/// Extension trait for the `DatabaseConnection` to work with the whitelist +/// table +pub trait UsersStatusExt { + /// Returns true if the `whitelister` has whitelisted the + /// `target_public_key` + async fn is_whitelisted( + &self, + whitelister: &UserModel, + target_public_key: &PublicKey, + ) -> ServerResult; + + /// Returns true if the `blacklister` has blacklisted the + /// `target_public_key` + async fn is_blacklisted( + &self, + blacklister: &UserModel, + target_public_key: &PublicKey, + ) -> ServerResult; + + /// Add the `target_public_key` to the whitelist of the `whitelister` and + /// remove it from the blacklist table (if it's there) + async fn add_to_whitelist( + &self, + whitelister: &UserModel, + target_public_key: &PublicKey, + ) -> ServerResult<()>; + + /// Add the `target_public_key` to the blacklist of the `blacklister` and + /// remove it from the whitelist table (if it's there) + async fn add_to_blacklist( + &self, + blacklister: &UserModel, + target_public_key: &PublicKey, + ) -> ServerResult<()>; + + /// Remove the target from whitelist table + // FIXME(awiteb): This method will be used when I work on decentralization, So, I'm keeping it + // for now + #[allow(dead_code)] + async fn remove_from_whitelist( + &self, + whitelister: &UserModel, + target_public_key: &PublicKey, + ) -> ServerResult<()>; + + /// Remove the target from blacklist table + // FIXME(awiteb): This method will be used when I work on decentralization, So, I'm keeping it + // for now + #[allow(dead_code)] + async fn remove_from_blacklist( + &self, + blacklister: &UserModel, + target_public_key: &PublicKey, + ) -> ServerResult<()>; + + /// Returns the whitelist of the user + async fn user_whitelist( + &self, + whitelister: &UserModel, + page: NonZeroU32, + page_size: NonZeroU8, + ) -> ServerResult>; + + /// Returns the blacklist of the user + async fn user_blacklist( + &self, + blacklister: &UserModel, + page: NonZeroU32, + page_size: NonZeroU8, + ) -> ServerResult>; +} + +impl UsersStatusExt for DatabaseConnection { + #[logcall::logcall] + async fn is_whitelisted( + &self, + whitelister: &UserModel, + target_public_key: &PublicKey, + ) -> ServerResult { + get_user_status( + self, + whitelister, + target_public_key, + AccessStatus::Whitelisted, + ) + .await + .map(|u| u.is_some()) + .map_err(Into::into) + } + + #[logcall::logcall] + async fn is_blacklisted( + &self, + blacklister: &UserModel, + target_public_key: &PublicKey, + ) -> ServerResult { + get_user_status( + self, + blacklister, + target_public_key, + AccessStatus::Blacklisted, + ) + .await + .map(|u| u.is_some()) + .map_err(Into::into) + } + + #[logcall::logcall] + async fn add_to_whitelist( + &self, + whitelister: &UserModel, + target_public_key: &PublicKey, + ) -> ServerResult<()> { + if whitelister.public_key == target_public_key.to_string() { + return Err(WsError::CannotAddSelfToWhitelist.into()); + } + + if let Some(mut user) = get_user_status( + self, + whitelister, + target_public_key, + AccessStatus::Blacklisted, + ) + .await? + .map(IntoActiveModel::into_active_model) + { + user.status = Set(AccessStatus::Whitelisted); + user.updated_at = Set(Utc::now()); + user.update(self).await?; + } else if let Err(err) = (UsersStatusActiveModel { + user_id: Set(whitelister.id), + target: Set(target_public_key.to_string()), + status: Set(AccessStatus::Whitelisted), + updated_at: Set(Utc::now()), + ..Default::default() + } + .save(self) + .await) + { + match err.sql_err() { + Some(SqlErr::UniqueConstraintViolation(_)) => { + return Err(WsError::AlreadyOnTheWhitelist.into()); + } + _ => return Err(err.into()), + } + } + + Ok(()) + } + + #[logcall::logcall] + async fn add_to_blacklist( + &self, + blacklister: &UserModel, + target_public_key: &PublicKey, + ) -> ServerResult<()> { + if blacklister.public_key == target_public_key.to_string() { + return Err(WsError::CannotAddSelfToBlacklist.into()); + } + + if let Some(mut user) = get_user_status( + self, + blacklister, + target_public_key, + AccessStatus::Whitelisted, + ) + .await? + .map(IntoActiveModel::into_active_model) + { + user.status = Set(AccessStatus::Blacklisted); + user.updated_at = Set(Utc::now()); + user.update(self).await?; + } else if let Err(err) = (UsersStatusActiveModel { + user_id: Set(blacklister.id), + target: Set(target_public_key.to_string()), + status: Set(AccessStatus::Blacklisted), + updated_at: Set(Utc::now()), + ..Default::default() + } + .save(self) + .await) + { + match err.sql_err() { + Some(SqlErr::UniqueConstraintViolation(_)) => { + return Err(WsError::AlreadyOnTheBlacklist.into()); + } + _ => return Err(err.into()), + } + } + + Ok(()) + } + + #[logcall::logcall] + async fn remove_from_whitelist( + &self, + whitelister: &UserModel, + target_public_key: &PublicKey, + ) -> ServerResult<()> { + if let Some(target_user) = get_user_status( + self, + whitelister, + target_public_key, + AccessStatus::Whitelisted, + ) + .await? + { + target_user.delete(self).await?; + } + Ok(()) + } + + #[logcall::logcall] + async fn remove_from_blacklist( + &self, + blacklister: &UserModel, + target_public_key: &PublicKey, + ) -> ServerResult<()> { + if let Some(target_user) = get_user_status( + self, + blacklister, + target_public_key, + AccessStatus::Blacklisted, + ) + .await? + { + target_user.delete(self).await?; + } + Ok(()) + } + + async fn user_whitelist( + &self, + whitelister: &UserModel, + page: NonZeroU32, + page_size: NonZeroU8, + ) -> ServerResult> { + whitelister + .find_related(UsersStatusEntity) + .filter(UsersStatusColumn::Status.eq(AccessStatus::Whitelisted)) + .paginate(self, u64::from(page_size.get())) + .fetch_page(u64::from(page.get() - 1)) + .await + .map_err(Into::into) + } + + async fn user_blacklist( + &self, + blacklister: &UserModel, + page: NonZeroU32, + page_size: NonZeroU8, + ) -> ServerResult> { + blacklister + .find_related(UsersStatusEntity) + .filter(UsersStatusColumn::Status.eq(AccessStatus::Blacklisted)) + .paginate(self, u64::from(page_size.get())) + .fetch_page(u64::from(page.get() - 1)) + .await + .map_err(Into::into) + } +} + +/// Returns user from user_status table by the entered and target public key +async fn get_user_status( + conn: &DatabaseConnection, + user: &UserModel, + target_public_key: &PublicKey, + status: AccessStatus, +) -> ServerResult> { + user.find_related(UsersStatusEntity) + .filter( + UsersStatusColumn::Target + .eq(target_public_key.to_string()) + .and(UsersStatusColumn::Status.eq(status)), + ) + .one(conn) + .await + .map_err(Into::into) +} diff --git a/crates/oxidetalis/src/errors.rs b/crates/oxidetalis/src/errors.rs index 95f6b2f..f9b5057 100644 --- a/crates/oxidetalis/src/errors.rs +++ b/crates/oxidetalis/src/errors.rs @@ -14,24 +14,16 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -use salvo::{ - http::StatusCode, - oapi::{Components as OapiComponents, EndpointOutRegister, Operation as OapiOperation}, - Response, - Scribe, -}; +use sea_orm::DbErr; -use crate::{routes::write_json_body, schemas::MessageSchema}; +use crate::{routes::ApiError, websocket::errors::WsError}; /// Result type of the homeserver -#[allow(clippy::absolute_paths)] -pub(crate) type Result = std::result::Result; -#[allow(clippy::absolute_paths)] -pub type ApiResult = std::result::Result; +pub(crate) type ServerResult = Result; /// The homeserver errors #[derive(Debug, thiserror::Error)] -pub(crate) enum Error { +pub enum InternalError { #[error("Database Error: {0}")] Database(#[from] sea_orm::DbErr), #[error("{0}")] @@ -39,43 +31,41 @@ pub(crate) enum Error { } #[derive(Debug, thiserror::Error)] -pub enum ApiError { - /// Error from the database (500 Internal Server Error) - #[error("Internal server error")] - SeaOrm(#[from] sea_orm::DbErr), - /// The server registration is closed (403 Forbidden) - #[error("Server registration is closed")] - RegistrationClosed, - /// The entered public key is already registered (400 Bad Request) - #[error("The entered public key is already registered")] - DuplicatedUser, - /// The user entered two different public keys - /// one in the header and other in the request body - /// (400 Bad Request) - #[error("You entered two different public keys")] - TwoDifferentKeys, +/// The homeserver errors +pub enum ServerError { + /// Internal server error, should not be exposed to the user + #[error("{0}")] + Internal(#[from] InternalError), + /// API error, errors happening in the API + #[error("{0}")] + Api(#[from] ApiError), + /// WebSocket error, errors happening in the WebSocket + #[error("{0}")] + Ws(#[from] WsError), } -impl ApiError { - /// Status code of the error - pub const fn status_code(&self) -> StatusCode { - match self { - Self::SeaOrm(_) => StatusCode::INTERNAL_SERVER_ERROR, - Self::RegistrationClosed => StatusCode::FORBIDDEN, - Self::DuplicatedUser | Self::TwoDifferentKeys => StatusCode::BAD_REQUEST, +impl From for ServerError { + fn from(err: DbErr) -> Self { + Self::Internal(err.into()) + } +} + +impl From for WsError { + fn from(err: ServerError) -> Self { + match err { + ServerError::Api(ApiError::NotRegisteredUser) => WsError::RegistredUserEvent, + ServerError::Internal(_) | ServerError::Api(_) => WsError::InternalServerError, + ServerError::Ws(err) => err, } } } -impl EndpointOutRegister for ApiError { - fn register(_: &mut OapiComponents, _: &mut OapiOperation) {} -} - -impl Scribe for ApiError { - fn render(self, res: &mut Response) { - log::error!("Error: {self}"); - - res.status_code(self.status_code()); - write_json_body(res, MessageSchema::new(self.to_string())); +impl From for ApiError { + fn from(err: ServerError) -> Self { + match err { + ServerError::Ws(WsError::RegistredUserEvent) => ApiError::NotRegisteredUser, + ServerError::Internal(_) | ServerError::Ws(_) => ApiError::Internal, + ServerError::Api(err) => err, + } } } diff --git a/crates/oxidetalis/src/extensions.rs b/crates/oxidetalis/src/extensions.rs index 3f4415e..fb1eab3 100644 --- a/crates/oxidetalis/src/extensions.rs +++ b/crates/oxidetalis/src/extensions.rs @@ -18,20 +18,21 @@ use std::sync::Arc; use chrono::Utc; use oxidetalis_config::Config; +use oxidetalis_core::types::PublicKey; use rayon::iter::{IntoParallelRefMutIterator, ParallelIterator}; -use salvo::{websocket::Message, Depot}; +use salvo::Depot; use sea_orm::DatabaseConnection; use uuid::Uuid; use crate::{ nonce::NonceCache, - websocket::{OnlineUsers, ServerEvent, SocketUserData}, + websocket::{OnlineUsers, ServerEvent, SocketUserData, Unsigned}, }; /// Extension trait for the Depot. pub trait DepotExt { /// Returns the database connection - fn db_conn(&self) -> &DatabaseConnection; + fn db_conn(&self) -> Arc; /// Returns the server configuration fn config(&self) -> &Config; /// Retutns the nonce cache @@ -54,12 +55,20 @@ pub trait OnlineUsersExt { /// Disconnect inactive users (who not respond for the ping event) async fn disconnect_inactive_users(&self); + + /// Returns the connection id of the user, if it is online + async fn is_online(&self, public_key: &PublicKey) -> Option; + + /// Send an event to user by connection id + async fn send(&self, conn_id: &Uuid, event: ServerEvent); } impl DepotExt for Depot { - fn db_conn(&self) -> &DatabaseConnection { - self.obtain::>() - .expect("Database connection not found") + fn db_conn(&self) -> Arc { + Arc::clone( + self.obtain::>() + .expect("Database connection not found"), + ) } fn config(&self) -> &Config { @@ -87,9 +96,10 @@ impl OnlineUsersExt for OnlineUsers { let now = Utc::now(); self.write().await.par_iter_mut().for_each(|(_, u)| { u.pinged_at = now; - let _ = u.sender.unbounded_send(Ok(Message::from( - &ServerEvent::ping().sign(&u.shared_secret), - ))); + let _ = u.sender.unbounded_send(Ok(ServerEvent::ping() + .sign(&u.shared_secret) + .as_ref() + .into())); }); } @@ -110,4 +120,20 @@ impl OnlineUsersExt for OnlineUsers { true }); } + + async fn is_online(&self, public_key: &PublicKey) -> Option { + self.read() + .await + .iter() + .find(|(_, u)| &u.public_key == public_key) + .map(|(c, _)| *c) + } + + async fn send(&self, conn_id: &Uuid, event: ServerEvent) { + if let Some(user) = self.read().await.get(conn_id) { + let _ = user + .sender + .unbounded_send(Ok(event.sign(&user.shared_secret).as_ref().into())); + } + } } diff --git a/crates/oxidetalis/src/macros.rs b/crates/oxidetalis/src/macros.rs new file mode 100644 index 0000000..f2d95e2 --- /dev/null +++ b/crates/oxidetalis/src/macros.rs @@ -0,0 +1,93 @@ +// OxideTalis Messaging Protocol homeserver implementation +// Copyright (C) 2024 OxideTalis Developers +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +//! OxideTalis server macros, to make the code more readable and easier to +//! write. + +/// Macro to return a [`ServerEvent`] with a [`WsError::InternalServerError`] if +/// the result of an expression is an [`Err`]. +/// +/// ## Example +/// ```rust,ignore +/// fn example() -> ServerEvent { +/// // some_function() returns a Result, if it's an Err, return an +/// // ServerEvent::InternalServerError +/// let result = try_ws!(some_function()); +/// ServerEvent::from(result) +/// } +/// ``` +/// +/// [`ServerEvent`]: crate::websocket::ServerEvent +/// [`WsError::InternalServerError`]: crate::websocket::errors::WsError::InternalServerError +/// [`Err`]: std::result::Result::Err +#[macro_export] +macro_rules! try_ws { + (Some $result_expr:expr) => { + match $result_expr { + Ok(val) => val, + Err(err) => { + log::error!("{err}"); + return Some( + $crate::websocket::ServerEvent::<$crate::websocket::Unsigned>::from( + $crate::websocket::errors::WsError::from(err), + ), + ); + } + } + }; +} + +/// Macro to create the `WsError` enum with the given error names and reasons. +/// +/// ## Example +/// ```rust,ignore +/// ws_errors! { +/// FirstError = "This is the first error", +/// SecondError = "This is the second error", +/// } +/// ``` +#[macro_export] +macro_rules! ws_errors { + ($($name:ident = $reason:tt),+ $(,)?) => { + #[derive(Debug, thiserror::Error)] + #[doc = "Websocket errors, returned in the websocket communication"] + pub enum WsError { + $( + #[doc = $reason] + #[error($reason)] + $name + ),+ + } + impl WsError { + #[doc = "Returns error name"] + pub const fn name(&self) -> &'static str { + match self { + $( + WsError::$name => stringify!($name) + ),+ + } + } + #[doc = "Returns the error reason"] + pub const fn reason(&self) -> &'static str { + match self { + $( + WsError::$name => $reason + ),+ + } + } + } + }; +} diff --git a/crates/oxidetalis/src/main.rs b/crates/oxidetalis/src/main.rs index 53ad66d..637cefc 100644 --- a/crates/oxidetalis/src/main.rs +++ b/crates/oxidetalis/src/main.rs @@ -19,6 +19,7 @@ use std::process::ExitCode; +use errors::ServerError; use oxidetalis_config::{CliArgs, Parser}; use oxidetalis_migrations::MigratorTrait; use salvo::{conn::TcpListener, Listener, Server}; @@ -26,18 +27,21 @@ use salvo::{conn::TcpListener, Listener, Server}; mod database; mod errors; mod extensions; +mod macros; mod middlewares; mod nonce; +mod parameters; mod routes; mod schemas; mod utils; mod websocket; -async fn try_main() -> errors::Result<()> { +async fn try_main() -> errors::ServerResult<()> { pretty_env_logger::init_timed(); log::info!("Parsing configuration"); - let config = oxidetalis_config::Config::load(CliArgs::parse())?; + let config = oxidetalis_config::Config::load(CliArgs::parse()) + .map_err(|err| ServerError::Internal(err.into()))?; log::info!("Configuration parsed successfully"); log::info!("Connecting to the database"); let connection = sea_orm::Database::connect(utils::postgres_url(&config.postgresdb)).await?; diff --git a/crates/oxidetalis/src/parameters/mod.rs b/crates/oxidetalis/src/parameters/mod.rs new file mode 100644 index 0000000..4fea6e9 --- /dev/null +++ b/crates/oxidetalis/src/parameters/mod.rs @@ -0,0 +1,21 @@ +// OxideTalis Messaging Protocol homeserver implementation +// Copyright (C) 2024 OxideTalis Developers +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +//! Set of route parameters for the API + +mod pagination; + +pub use pagination::*; diff --git a/crates/oxidetalis/src/parameters/pagination.rs b/crates/oxidetalis/src/parameters/pagination.rs new file mode 100644 index 0000000..5dab644 --- /dev/null +++ b/crates/oxidetalis/src/parameters/pagination.rs @@ -0,0 +1,131 @@ +// OxideTalis Messaging Protocol homeserver implementation +// Copyright (C) 2024 OxideTalis Developers +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +//! Pagination parameters for the API + +use std::{ + fmt, + num::{NonZeroU32, NonZeroU8}, + str::FromStr, +}; + +use salvo::{ + extract::Metadata as ExtractMetadata, + oapi::{ + Components as OapiComponents, + EndpointArgRegister, + Object, + Operation as OapiOperation, + Parameter, + ParameterIn, + Parameters, + SchemaType, + ToParameters, + }, + Extractible, + Request, +}; +use serde_json::json; + +use crate::routes::{ApiError, ApiResult}; + +#[derive(Debug)] +pub struct Pagination { + /// The page number of the result + pub page: NonZeroU32, + /// The page size + pub page_size: NonZeroU8, +} + +impl<'ex> Extractible<'ex> for Pagination { + fn metadata() -> &'ex ExtractMetadata { + static METADATA: ExtractMetadata = ExtractMetadata::new(""); + &METADATA + } + + #[allow(refining_impl_trait)] + async fn extract(req: &'ex mut Request) -> ApiResult { + let page = extract_query(req, "page", NonZeroU32::new(1).expect("is non-zero"))?; + let page_size = extract_query(req, "page_size", NonZeroU8::new(10).expect("is non-zero"))?; + + Ok(Self { page, page_size }) + } + + #[allow(refining_impl_trait)] + async fn extract_with_arg(req: &'ex mut Request, _arg: &str) -> ApiResult { + Self::extract(req).await + } +} + +impl ToParameters<'_> for Pagination { + fn to_parameters(_components: &mut OapiComponents) -> Parameters { + Parameters::new() + .parameter(create_parameter( + "page", + "Page number, starting from 1", + 1, + f64::from(u32::MAX), + )) + .parameter(create_parameter( + "page_size", + "How many items per page, starting from 1", + 10, + f64::from(u8::MAX), + )) + } +} + +impl EndpointArgRegister for Pagination { + fn register(components: &mut OapiComponents, operation: &mut OapiOperation, _arg: &str) { + for parameter in Self::to_parameters(components) { + operation.parameters.insert(parameter); + } + } +} + +/// Extract a query parameter from the request +fn extract_query(req: &Request, name: &str, default_value: T) -> ApiResult +where + ::Err: fmt::Display, +{ + Ok(req + .queries() + .get(name) + .map(|p| p.parse::()) + .transpose() + .map_err(|err| ApiError::Querys(format!("Invalid value for `{name}` query ({err})")))? + .unwrap_or(default_value)) +} + +/// Create a parameter for the pagination +fn create_parameter(name: &str, description: &str, default: usize, max: f64) -> Parameter { + Parameter::new(name) + .parameter_in(ParameterIn::Query) + .required(false) + .description(description) + .example(json!(default)) + .schema( + Object::new() + .name(name) + .description(description) + .schema_type(SchemaType::Integer) + .default_value(json!(default)) + .example(json!(default)) + .maximum(max) + .minimum(1.0) + .read_only(true), + ) +} diff --git a/crates/oxidetalis/src/routes/errors.rs b/crates/oxidetalis/src/routes/errors.rs new file mode 100644 index 0000000..788559b --- /dev/null +++ b/crates/oxidetalis/src/routes/errors.rs @@ -0,0 +1,77 @@ +// OxideTalis Messaging Protocol homeserver implementation +// Copyright (C) 2024 OxideTalis Developers +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +use salvo::{ + http::StatusCode, + oapi::{Components as OapiComponents, EndpointOutRegister, Operation as OapiOperation}, + Response, + Scribe, +}; + +use crate::{routes::write_json_body, schemas::MessageSchema}; + +/// Result type of the API +pub type ApiResult = Result; + +#[derive(Debug, thiserror::Error)] +pub enum ApiError { + #[error("Internal server error")] + Internal, + /// The server registration is closed (403 Forbidden) + #[error("Server registration is closed")] + RegistrationClosed, + /// The entered public key is already registered (400 Bad Request) + #[error("The entered public key is already registered")] + AlreadyRegistered, + /// The user entered two different public keys + /// one in the header and other in the request body + /// (400 Bad Request) + #[error("You entered two different public keys")] + TwoDifferentKeys, + /// Error in the query parameters (400 Bad Request) + #[error("{0}")] + Querys(String), + /// Non registered user tried to access to registered user only endpoint + /// (403 Forbidden) + #[error("You are not a registered user, please register first")] + NotRegisteredUser, +} + +impl ApiError { + /// Status code of the error + pub const fn status_code(&self) -> StatusCode { + match self { + Self::Internal => StatusCode::INTERNAL_SERVER_ERROR, + Self::RegistrationClosed | Self::NotRegisteredUser => StatusCode::FORBIDDEN, + Self::AlreadyRegistered | Self::TwoDifferentKeys | Self::Querys(_) => { + StatusCode::BAD_REQUEST + } + } + } +} + +impl EndpointOutRegister for ApiError { + fn register(_: &mut OapiComponents, _: &mut OapiOperation) {} +} + +impl Scribe for ApiError { + fn render(self, res: &mut Response) { + log::error!("Error: {self}"); + + res.status_code(self.status_code()); + write_json_body(res, MessageSchema::new(self.to_string())); + } +} diff --git a/crates/oxidetalis/src/routes/mod.rs b/crates/oxidetalis/src/routes/mod.rs index 45bdbd5..1923cb6 100644 --- a/crates/oxidetalis/src/routes/mod.rs +++ b/crates/oxidetalis/src/routes/mod.rs @@ -27,8 +27,11 @@ use crate::nonce::NonceCache; use crate::schemas::MessageSchema; use crate::{middlewares, websocket}; +mod errors; mod user; +pub use errors::*; + pub fn write_json_body(res: &mut Response, json_body: impl serde::Serialize) { res.write_body(serde_json::to_string(&json_body).expect("Json serialization can't be fail")) .ok(); diff --git a/crates/oxidetalis/src/routes/user.rs b/crates/oxidetalis/src/routes/user.rs index ccefe73..9f3b777 100644 --- a/crates/oxidetalis/src/routes/user.rs +++ b/crates/oxidetalis/src/routes/user.rs @@ -20,18 +20,20 @@ use oxidetalis_core::types::{PublicKey, Signature}; use salvo::{ http::StatusCode, oapi::{endpoint, extract::JsonBody}, + writing::Json, Depot, Request, Router, Writer, }; +use super::{ApiError, ApiResult}; use crate::{ - database::UserTableExt, - errors::{ApiError, ApiResult}, + database::{UserTableExt, UsersStatusExt}, extensions::DepotExt, middlewares, - schemas::{EmptySchema, MessageSchema, RegisterUserBody}, + parameters::Pagination, + schemas::{BlackListedUser, EmptySchema, MessageSchema, RegisterUserBody, WhiteListedUser}, utils, }; @@ -79,10 +81,90 @@ pub async fn register( Ok(EmptySchema::new(StatusCode::CREATED)) } +/// (🔐) Get whitelisted users +#[endpoint( + operation_id = "whitelist", + tags("User"), + responses( + (status_code = 200, description = "Returns whitelisted users", content_type = "application/json", body = Vec), + (status_code = 400, description = "Wrong query parameter", content_type = "application/json", body = MessageSchema), + (status_code = 401, description = "The entered signature or public key is invalid", content_type = "application/json", body = MessageSchema), + (status_code = 403, description = "Not registered user, must register first", content_type = "application/json", body = MessageSchema), + (status_code = 429, description = "Too many requests", content_type = "application/json", body = MessageSchema), + (status_code = 500, description = "Internal server error", content_type = "application/json", body = MessageSchema), + ), + parameters( + ("X-OTMP-PUBLIC" = PublicKey, Header, description = "Public key of the sender"), + ("X-OTMP-SIGNATURE" = Signature, Header, description = "Signature of the request"), + ), +)] +async fn user_whitelist( + req: &mut Request, + depot: &mut Depot, + pagination: Pagination, +) -> ApiResult>> { + let conn = depot.db_conn(); + let user = conn + .get_user_by_pubk( + &utils::extract_public_key(req) + .expect("Public key should be checked in the middleware"), + ) + .await? + .ok_or(ApiError::NotRegisteredUser)?; + Ok(Json( + conn.user_whitelist(&user, pagination.page, pagination.page_size) + .await? + .into_iter() + .map(Into::into) + .collect(), + )) +} + +/// (🔐) Get blacklisted users +#[endpoint( + operation_id = "blacklist", + tags("User"), + responses( + (status_code = 200, description = "Returns blacklisted users", content_type = "application/json", body = Vec), + (status_code = 400, description = "Wrong query parameter", content_type = "application/json", body = MessageSchema), + (status_code = 401, description = "The entered signature or public key is invalid", content_type = "application/json", body = MessageSchema), + (status_code = 403, description = "Not registered user, must register first", content_type = "application/json", body = MessageSchema), + (status_code = 429, description = "Too many requests", content_type = "application/json", body = MessageSchema), + (status_code = 500, description = "Internal server error", content_type = "application/json", body = MessageSchema), + ), + parameters( + ("X-OTMP-PUBLIC" = PublicKey, Header, description = "Public key of the sender"), + ("X-OTMP-SIGNATURE" = Signature, Header, description = "Signature of the request"), + ), +)] +async fn user_blacklist( + req: &mut Request, + depot: &mut Depot, + pagination: Pagination, +) -> ApiResult>> { + let conn = depot.db_conn(); + let user = conn + .get_user_by_pubk( + &utils::extract_public_key(req) + .expect("Public key should be checked in the middleware"), + ) + .await? + .ok_or(ApiError::NotRegisteredUser)?; + Ok(Json( + conn.user_blacklist(&user, pagination.page, pagination.page_size) + .await? + .into_iter() + .map(Into::into) + .collect(), + )) +} + /// The route of the endpoints of this module pub fn route() -> Router { Router::new() .push(Router::with_path("register").post(register)) + .push(Router::with_path("whitelist").get(user_whitelist)) + .push(Router::with_path("blacklist").get(user_blacklist)) .hoop(middlewares::public_key_check) .hoop(middlewares::signature_check) } diff --git a/crates/oxidetalis/src/schemas/user.rs b/crates/oxidetalis/src/schemas/user.rs index 321ce28..4351d77 100644 --- a/crates/oxidetalis/src/schemas/user.rs +++ b/crates/oxidetalis/src/schemas/user.rs @@ -14,7 +14,11 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . +use std::str::FromStr; + +use chrono::{DateTime, Utc}; use oxidetalis_core::{cipher::K256Secret, types::PublicKey}; +use oxidetalis_entities::prelude::*; use salvo::oapi::ToSchema; use serde::{Deserialize, Serialize}; @@ -25,3 +29,57 @@ pub struct RegisterUserBody { /// The public key of the user pub public_key: PublicKey, } + +#[derive(Serialize, Deserialize, Clone, Debug, ToSchema, derive_new::new)] +#[salvo(schema(name = WhiteListedUser, example = json!(WhiteListedUser::default())))] +pub struct WhiteListedUser { + /// User's public key + pub public_key: PublicKey, + /// When the user was whitelisted + pub whitelisted_at: DateTime, +} + +#[derive(Serialize, Deserialize, Clone, Debug, ToSchema, derive_new::new)] +#[salvo(schema(name = BlackListedUser, example = json!(BlackListedUser::default())))] +pub struct BlackListedUser { + /// User's public key + pub public_key: PublicKey, + /// When the user was blacklisted + pub blacklisted_at: DateTime, +} + +impl Default for WhiteListedUser { + fn default() -> Self { + WhiteListedUser::new( + PublicKey::from_str("bYhbrm61ov8GLZfskUYbsCLJTfaacMsuTBYgBABEH9dy").expect("is valid"), + Utc::now(), + ) + } +} + +impl From for WhiteListedUser { + fn from(user: UsersStatusModel) -> Self { + Self { + public_key: PublicKey::from_str(&user.target).expect("Is valid public key"), + whitelisted_at: user.updated_at, + } + } +} + +impl Default for BlackListedUser { + fn default() -> Self { + BlackListedUser::new( + PublicKey::from_str("bYhbrm61ov8GLZfskUYbsCLJTfaacMsuTBYgBABEH9dy").expect("is valid"), + Utc::now(), + ) + } +} + +impl From for BlackListedUser { + fn from(user: UsersStatusModel) -> Self { + Self { + public_key: PublicKey::from_str(&user.target).expect("Is valid public key"), + blacklisted_at: user.updated_at, + } + } +} diff --git a/crates/oxidetalis/src/websocket/errors.rs b/crates/oxidetalis/src/websocket/errors.rs index b05e550..036801d 100644 --- a/crates/oxidetalis/src/websocket/errors.rs +++ b/crates/oxidetalis/src/websocket/errors.rs @@ -16,42 +16,27 @@ //! Websocket errors +use crate::ws_errors; + /// Result type of websocket pub type WsResult = Result; -/// Websocket errors, returned in the websocket communication -#[derive(Debug)] -pub enum WsError { - /// The signature is invalid - InvalidSignature, - /// Message type must be text - NotTextMessage, - /// Invalid json data - InvalidJsonData, - /// Unknown client event - UnknownClientEvent, -} - -impl WsError { - /// Returns error name - pub const fn name(&self) -> &'static str { - match self { - WsError::InvalidSignature => "InvalidSignature", - WsError::NotTextMessage => "NotTextMessage", - WsError::InvalidJsonData => "InvalidJsonData", - WsError::UnknownClientEvent => "UnknownClientEvent", - } - } - - /// Returns the error reason - pub const fn reason(&self) -> &'static str { - match self { - WsError::InvalidSignature => "Invalid event signature", - WsError::NotTextMessage => "The websocket message must be text message", - WsError::InvalidJsonData => "Received invalid json data, the text must be valid json", - WsError::UnknownClientEvent => { - "Unknown client event, the event is not recognized by the server" - } - } - } +ws_errors! { + InternalServerError = "Internal server error", + InvalidSignature = "Invalid event signature", + NotTextMessage = "The websocket message must be text message", + InvalidJsonData = "Received invalid json data, the text must be valid json", + UnknownClientEvent = "Unknown client event, the event is not recognized by the server", + RegistredUserEvent = "The event is only for registred users", + UserNotFound = "The user is not registered in the server", + AlreadyOnTheWhitelist = "The user is already on your whitelist", + CannotAddSelfToWhitelist = "You cannot add yourself to the whitelist", + AlreadyOnTheBlacklist = "The user is already on your blacklist", + CannotAddSelfToBlacklist = "You cannot add yourself to the blacklist", + AlreadySendChatRequest = "You have already sent a chat request to this user", + CannotSendChatRequestToSelf = "You cannot send a chat request to yourself", + CannotRespondToOwnChatRequest = "You cannot respond to your own chat request", + NoChatRequestFromRecipient = "You do not have a chat request from the recipient", + RecipientBlacklist = "You cannot send a chat request because you are on the recipient's blacklist.", + AlreadyInRecipientWhitelist = "You are already on the recipient's whitelist and can chat with them." } diff --git a/crates/oxidetalis/src/websocket/events/client.rs b/crates/oxidetalis/src/websocket/events/client.rs index a4dda31..524c3e1 100644 --- a/crates/oxidetalis/src/websocket/events/client.rs +++ b/crates/oxidetalis/src/websocket/events/client.rs @@ -16,7 +16,7 @@ //! Events that the client send it -use oxidetalis_core::types::Signature; +use oxidetalis_core::types::{PublicKey, Signature}; use serde::{Deserialize, Serialize}; use crate::{nonce::NonceCache, utils}; @@ -24,10 +24,14 @@ use crate::{nonce::NonceCache, utils}; /// Client websocket event #[derive(Deserialize, Clone, Debug)] pub struct ClientEvent { + #[serde(flatten)] pub event: ClientEventType, signature: Signature, } +// ## Important for contuributors +// Please make sure to order the event data alphabetically. + /// Client websocket event type #[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Debug)] #[serde(rename_all = "PascalCase", tag = "event", content = "data")] @@ -36,6 +40,10 @@ pub enum ClientEventType { Ping { timestamp: u64 }, /// Pong event Pong { timestamp: u64 }, + /// Request to chat with a user + ChatRequest { to: PublicKey }, + /// Response to a chat request + ChatRequestResponse { accepted: bool, to: PublicKey }, } impl ClientEventType { diff --git a/crates/oxidetalis/src/websocket/events/server.rs b/crates/oxidetalis/src/websocket/events/server.rs index 225b129..6295891 100644 --- a/crates/oxidetalis/src/websocket/events/server.rs +++ b/crates/oxidetalis/src/websocket/events/server.rs @@ -19,7 +19,10 @@ use std::marker::PhantomData; use chrono::Utc; -use oxidetalis_core::{cipher::K256Secret, types::Signature}; +use oxidetalis_core::{ + cipher::K256Secret, + types::{PublicKey, Signature}, +}; use salvo::websocket::Message; use serde::Serialize; @@ -28,6 +31,7 @@ use crate::websocket::errors::WsError; /// Signed marker, used to indicate that the event is signed pub struct Signed; /// Unsigned marker, used to indicate that the event is unsigned +#[derive(Debug)] pub struct Unsigned; /// Server websocket event @@ -42,12 +46,16 @@ pub struct ServerEvent { /// server websocket event type #[derive(Serialize, Clone, Eq, PartialEq, Debug)] -#[serde(rename_all = "PascalCase")] +#[serde(rename_all = "PascalCase", tag = "event", content = "data")] pub enum ServerEventType { /// Ping event Ping { timestamp: u64 }, /// Pong event Pong { timestamp: u64 }, + /// New chat request from someone + ChatRequest { from: PublicKey }, + /// New chat request response from someone + ChatRequestResponse { accepted: bool, from: PublicKey }, /// Error event Error { name: &'static str, @@ -88,6 +96,16 @@ impl ServerEvent { }) } + /// Create chat request event + pub fn chat_request(from: &PublicKey) -> Self { + Self::new(ServerEventType::ChatRequest { from: *from }) + } + + /// Create chat request response event + pub fn chat_request_response(from: PublicKey, accepted: bool) -> Self { + Self::new(ServerEventType::ChatRequestResponse { from, accepted }) + } + /// Sign the event pub fn sign(self, shared_secret: &[u8; 32]) -> ServerEvent { ServerEvent:: { @@ -101,6 +119,12 @@ impl ServerEvent { } } +impl AsRef for ServerEvent { + fn as_ref(&self) -> &Self { + self + } +} + impl From<&ServerEvent> for Message { fn from(value: &ServerEvent) -> Self { Message::text(serde_json::to_string(value).expect("This can't fail")) diff --git a/crates/oxidetalis/src/websocket/handlers/chat_request.rs b/crates/oxidetalis/src/websocket/handlers/chat_request.rs new file mode 100644 index 0000000..bb8639c --- /dev/null +++ b/crates/oxidetalis/src/websocket/handlers/chat_request.rs @@ -0,0 +1,138 @@ +// OxideTalis Messaging Protocol homeserver implementation +// Copyright (C) 2024 OxideTalis Developers +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +//! Handler for incoming and outgoing chat requests. + +use std::str::FromStr; + +use oxidetalis_core::types::PublicKey; +use oxidetalis_entities::prelude::*; +use sea_orm::DatabaseConnection; + +use crate::database::InChatRequestsExt; +use crate::errors::ServerError; +use crate::extensions::OnlineUsersExt; +use crate::{ + database::{OutChatRequestsExt, UserTableExt, UsersStatusExt}, + try_ws, + websocket::{errors::WsError, ServerEvent, Unsigned, ONLINE_USERS}, +}; + +/// Handle a chat request from a user. +#[logcall::logcall] +pub async fn handle_chat_request( + db: &DatabaseConnection, + from: Option<&UserModel>, + to_public_key: &PublicKey, +) -> Option> { + let Some(from_user) = from else { + return Some(WsError::RegistredUserEvent.into()); + }; + let Some(to_user) = try_ws!(Some db.get_user_by_pubk(to_public_key).await) else { + return Some(WsError::UserNotFound.into()); + }; + if from_user.id == to_user.id { + return Some(WsError::CannotSendChatRequestToSelf.into()); + } + // FIXME: When change the entity public key to a PublicKey type, change this + let from_public_key = PublicKey::from_str(&from_user.public_key).expect("Is valid public key"); + + if try_ws!(Some db.get_chat_request_to(from_user, to_public_key).await).is_some() { + return Some(WsError::AlreadySendChatRequest.into()); + } + + if try_ws!(Some db.is_blacklisted(&to_user, &from_public_key).await) { + return Some(WsError::RecipientBlacklist.into()); + } + + // To ignore the error if the requester added the recipient to the whitelist + // table before send a request to them + if let Err(ServerError::Internal(_)) = db.add_to_whitelist(from_user, to_public_key).await { + return Some(WsError::InternalServerError.into()); + } + + if try_ws!(Some db.is_whitelisted(&to_user, &from_public_key).await) { + return Some(WsError::AlreadyInRecipientWhitelist.into()); + } + + try_ws!(Some db.save_out_chat_request(from_user, to_public_key).await); + if let Some(conn_id) = ONLINE_USERS.is_online(to_public_key).await { + ONLINE_USERS + .send(&conn_id, ServerEvent::chat_request(&from_public_key)) + .await; + } else { + try_ws!(Some db.save_in_chat_request(&from_public_key, &to_user).await); + } + None +} + +#[logcall::logcall] +pub async fn handle_chat_response( + db: &DatabaseConnection, + recipient: Option<&UserModel>, + sender_public_key: &PublicKey, + accepted: bool, +) -> Option> { + let Some(recipient_user) = recipient else { + return Some(WsError::RegistredUserEvent.into()); + }; + let Some(sender_user) = try_ws!(Some db.get_user_by_pubk(sender_public_key).await) else { + return Some(WsError::UserNotFound.into()); + }; + if recipient_user.id == sender_user.id { + return Some(WsError::CannotRespondToOwnChatRequest.into()); + } + + // FIXME: When change the entity public key to a PublicKey type, change this + let recipient_public_key = + PublicKey::from_str(&recipient_user.public_key).expect("Is valid public key"); + + if try_ws!(Some + db.get_chat_request_to(&sender_user, &recipient_public_key) + .await + ) + .is_none() + { + return Some(WsError::NoChatRequestFromRecipient.into()); + } + + // We don't need to handle the case where the sender is blacklisted or + // whitelisted already, just add it if it is not already there + let _ = if accepted { + db.add_to_whitelist(recipient_user, sender_public_key).await + } else { + db.add_to_blacklist(recipient_user, sender_public_key).await + }; + + try_ws!(Some + db.remove_out_chat_request(&sender_user, &recipient_public_key) + .await + ); + + if let Some(conn_id) = ONLINE_USERS.is_online(sender_public_key).await { + ONLINE_USERS + .send( + &conn_id, + ServerEvent::chat_request_response(recipient_public_key, accepted), + ) + .await; + } else { + // TODO: Create a table for chat request responses, and send them when + // the user logs in + } + + None +} diff --git a/crates/oxidetalis/src/websocket/handlers/mod.rs b/crates/oxidetalis/src/websocket/handlers/mod.rs new file mode 100644 index 0000000..5f632a3 --- /dev/null +++ b/crates/oxidetalis/src/websocket/handlers/mod.rs @@ -0,0 +1,21 @@ +// OxideTalis Messaging Protocol homeserver implementation +// Copyright (C) 2024 OxideTalis Developers +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +//! Websocket event handlers. + +mod chat_request; + +pub use chat_request::*; diff --git a/crates/oxidetalis/src/websocket/mod.rs b/crates/oxidetalis/src/websocket/mod.rs index c91479e..cd5d4ec 100644 --- a/crates/oxidetalis/src/websocket/mod.rs +++ b/crates/oxidetalis/src/websocket/mod.rs @@ -21,6 +21,7 @@ use errors::{WsError, WsResult}; use futures::{channel::mpsc, FutureExt, StreamExt, TryStreamExt}; use once_cell::sync::Lazy; use oxidetalis_core::{cipher::K256Secret, types::PublicKey}; +use oxidetalis_entities::prelude::*; use salvo::{ handler, http::StatusError, @@ -30,15 +31,18 @@ use salvo::{ Response, Router, }; +use sea_orm::DatabaseConnection; use tokio::{sync::RwLock, task::spawn as tokio_spawn, time::sleep as tokio_sleep}; -mod errors; +pub mod errors; mod events; +mod handlers; pub use events::*; use uuid::Uuid; use crate::{ + database::UserTableExt, extensions::{DepotExt, OnlineUsersExt}, middlewares, nonce::NonceCache, @@ -92,6 +96,7 @@ pub async fn user_connected( depot: &Depot, ) -> Result<(), StatusError> { let nonce_cache = depot.nonce_cache(); + let db_conn = depot.db_conn(); let public_key = utils::extract_public_key(req).expect("The public key was checked in the middleware"); // FIXME: The config should hold `K256Secret` not `PrivateKey` @@ -100,7 +105,7 @@ pub async fn user_connected( WebSocketUpgrade::new() .upgrade(req, res, move |ws| { - handle_socket(ws, nonce_cache, public_key, shared_secret) + handle_socket(ws, db_conn, nonce_cache, public_key, shared_secret) }) .await } @@ -108,6 +113,7 @@ pub async fn user_connected( /// Handle the websocket connection async fn handle_socket( ws: WebSocket, + db_conn: Arc, nonce_cache: Arc, user_public_key: PublicKey, user_shared_secret: [u8; 32], @@ -123,27 +129,47 @@ async fn handle_socket( }); tokio_spawn(fut); let conn_id = Uuid::new_v4(); - let user = SocketUserData::new(user_public_key, user_shared_secret, sender.clone()); - ONLINE_USERS.add_user(&conn_id, user).await; + let Ok(user) = db_conn.get_user_by_pubk(&user_public_key).await else { + let _ = sender.unbounded_send(Ok(ServerEvent::from(WsError::InternalServerError) + .sign(&user_shared_secret) + .as_ref() + .into())); + return; + }; + ONLINE_USERS + .add_user( + &conn_id, + SocketUserData::new(user_public_key, user_shared_secret, sender.clone()), + ) + .await; log::info!("New user connected: ConnId(={conn_id}) PublicKey(={user_public_key})"); + // TODO: Send the incoming chat request to the user, while they are offline. + // This after adding last_login col to the user table + let fut = async move { while let Some(Ok(msg)) = user_ws_receiver.next().await { match handle_ws_msg(msg, &nonce_cache, &user_shared_secret).await { Ok(event) => { - if let Some(server_event) = handle_events(event, &conn_id).await { - if let Err(err) = sender.unbounded_send(Ok(Message::from( - &server_event.sign(&user_shared_secret), - ))) { + if let Some(server_event) = + handle_events(event, &db_conn, &conn_id, user.as_ref()).await + { + if let Err(err) = sender.unbounded_send(Ok(server_event + .sign(&user_shared_secret) + .as_ref() + .into())) + { log::error!("Websocket Error: {err}"); break; } }; } Err(err) => { - if let Err(err) = sender.unbounded_send(Ok(Message::from( - &ServerEvent::from(err).sign(&user_shared_secret), - ))) { + if let Err(err) = sender.unbounded_send(Ok(ServerEvent::from(err) + .sign(&user_shared_secret) + .as_ref() + .into())) + { log::error!("Websocket Error: {err}"); break; }; @@ -178,13 +204,22 @@ async fn handle_ws_msg( } /// Handle user events, and return the server event if needed -async fn handle_events(event: ClientEvent, conn_id: &Uuid) -> Option> { +async fn handle_events( + event: ClientEvent, + db: &DatabaseConnection, + conn_id: &Uuid, + user: Option<&UserModel>, +) -> Option> { match &event.event { ClientEventType::Ping { .. } => Some(ServerEvent::pong()), ClientEventType::Pong { .. } => { ONLINE_USERS.update_pong(conn_id).await; None } + ClientEventType::ChatRequest { to } => handlers::handle_chat_request(db, user, to).await, + ClientEventType::ChatRequestResponse { to, accepted } => { + handlers::handle_chat_response(db, user, to, *accepted).await + } } } diff --git a/crates/oxidetalis_entities/Cargo.toml b/crates/oxidetalis_entities/Cargo.toml index b8f6e33..4a444f6 100644 --- a/crates/oxidetalis_entities/Cargo.toml +++ b/crates/oxidetalis_entities/Cargo.toml @@ -11,7 +11,8 @@ rust-version.workspace = true [dependencies] -sea-orm = {workspace = true } +sea-orm = { workspace = true } +chrono = { workspace = true } [lints.rust] unsafe_code = "deny" diff --git a/crates/oxidetalis_entities/src/incoming_chat_requests.rs b/crates/oxidetalis_entities/src/incoming_chat_requests.rs new file mode 100644 index 0000000..2d363cd --- /dev/null +++ b/crates/oxidetalis_entities/src/incoming_chat_requests.rs @@ -0,0 +1,57 @@ +// OxideTalis Messaging Protocol homeserver core implementation +// Copyright (c) 2024 OxideTalis Developers +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +use chrono::Utc; +use sea_orm::entity::prelude::*; + +use crate::prelude::*; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[sea_orm(table_name = "in_chat_requests")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: UserId, + pub recipient_id: UserId, + /// Public key of the sender + pub sender: String, + /// The timestamp of the request, when it was received + pub in_on: chrono::DateTime, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "UserEntity", + from = "Column::RecipientId", + to = "super::users::Column::Id" + on_update = "NoAction", + on_delete = "Cascade" + )] + RecipientId, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::RecipientId.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/oxidetalis_entities/src/lib.rs b/crates/oxidetalis_entities/src/lib.rs index 21ae991..7ed4e2c 100644 --- a/crates/oxidetalis_entities/src/lib.rs +++ b/crates/oxidetalis_entities/src/lib.rs @@ -19,5 +19,8 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. +pub mod incoming_chat_requests; +pub mod outgoing_chat_requests; pub mod prelude; pub mod users; +pub mod users_status; diff --git a/crates/oxidetalis_entities/src/outgoing_chat_requests.rs b/crates/oxidetalis_entities/src/outgoing_chat_requests.rs new file mode 100644 index 0000000..d083e3e --- /dev/null +++ b/crates/oxidetalis_entities/src/outgoing_chat_requests.rs @@ -0,0 +1,57 @@ +// OxideTalis Messaging Protocol homeserver core implementation +// Copyright (c) 2024 OxideTalis Developers +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +use chrono::Utc; +use sea_orm::entity::prelude::*; + +use crate::prelude::*; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[sea_orm(table_name = "out_chat_requests")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: UserId, + pub sender_id: UserId, + /// Public key of the recipient + pub recipient: String, + /// The timestamp of the request, when it was sent + pub out_on: chrono::DateTime, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "UserEntity", + from = "Column::SenderId", + to = "super::users::Column::Id" + on_update = "NoAction", + on_delete = "Cascade" + )] + SenderId, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::SenderId.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/oxidetalis_entities/src/prelude.rs b/crates/oxidetalis_entities/src/prelude.rs index ca81639..5adcf81 100644 --- a/crates/oxidetalis_entities/src/prelude.rs +++ b/crates/oxidetalis_entities/src/prelude.rs @@ -22,8 +22,10 @@ pub use sea_orm::{ ActiveModelTrait, ColumnTrait, + EntityOrSelect, EntityTrait, IntoActiveModel, + ModelTrait, Order, PaginatorTrait, QueryFilter, @@ -33,9 +35,31 @@ pub use sea_orm::{ SqlErr, }; +/// User ID type +pub type UserId = i64; + +pub use super::incoming_chat_requests::{ + ActiveModel as InChatRequestsActiveModel, + Column as InChatRequestsColumn, + Entity as InChatRequestsEntity, + Model as InChatRequestsModel, +}; +pub use super::outgoing_chat_requests::{ + ActiveModel as OutChatRequestsActiveModel, + Column as OutChatRequestsColumn, + Entity as OutChatRequestsEntity, + Model as OutChatRequestsModel, +}; pub use super::users::{ ActiveModel as UserActiveModel, Column as UserColumn, Entity as UserEntity, Model as UserModel, }; +pub use super::users_status::{ + AccessStatus, + ActiveModel as UsersStatusActiveModel, + Column as UsersStatusColumn, + Entity as UsersStatusEntity, + Model as UsersStatusModel, +}; diff --git a/crates/oxidetalis_entities/src/users.rs b/crates/oxidetalis_entities/src/users.rs index 1bae627..675e589 100644 --- a/crates/oxidetalis_entities/src/users.rs +++ b/crates/oxidetalis_entities/src/users.rs @@ -21,16 +21,43 @@ use sea_orm::entity::prelude::*; +use crate::prelude::*; + #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] #[sea_orm(table_name = "users")] pub struct Model { #[sea_orm(primary_key)] - pub id: i32, + pub id: UserId, pub public_key: String, pub is_admin: bool, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} +pub enum Relation { + #[sea_orm(has_many = "InChatRequestsEntity")] + InChatRequests, + #[sea_orm(has_many = "OutChatRequestsEntity")] + OutChatRequests, + #[sea_orm(has_many = "UsersStatusEntity")] + UsersStatus, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::InChatRequests.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::OutChatRequests.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::UsersStatus.def() + } +} impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/oxidetalis_entities/src/users_status.rs b/crates/oxidetalis_entities/src/users_status.rs new file mode 100644 index 0000000..3459192 --- /dev/null +++ b/crates/oxidetalis_entities/src/users_status.rs @@ -0,0 +1,66 @@ +// OxideTalis Messaging Protocol homeserver core implementation +// Copyright (c) 2024 OxideTalis Developers +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +use chrono::Utc; +use sea_orm::entity::prelude::*; + +use crate::prelude::*; + +#[derive(Debug, Clone, Eq, PartialEq, EnumIter, DeriveActiveEnum)] +#[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "access_status")] +pub enum AccessStatus { + #[sea_orm(string_value = "whitelisted")] + Whitelisted, + #[sea_orm(string_value = "blacklisted")] + Blacklisted, +} + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[sea_orm(table_name = "users_status")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: UserId, + pub user_id: UserId, + /// Public key of the target + pub target: String, + pub status: AccessStatus, + pub updated_at: chrono::DateTime, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "UserEntity", + from = "Column::UserId", + to = "super::users::Column::Id" + on_update = "NoAction", + on_delete = "Cascade" + )] + UserId, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::UserId.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/oxidetalis_migrations/src/create_incoming_chat_requests_table.rs b/crates/oxidetalis_migrations/src/create_incoming_chat_requests_table.rs new file mode 100644 index 0000000..51113e0 --- /dev/null +++ b/crates/oxidetalis_migrations/src/create_incoming_chat_requests_table.rs @@ -0,0 +1,89 @@ +// OxideTalis Messaging Protocol homeserver core implementation +// Copyright (c) 2024 OxideTalis Developers +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +use sea_orm_migration::prelude::*; + +use crate::create_users_table::Users; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_table( + Table::create() + .table(InChatRequests::Table) + .if_not_exists() + .col( + ColumnDef::new(InChatRequests::Id) + .big_integer() + .not_null() + .auto_increment() + .primary_key(), + ) + .col( + ColumnDef::new(InChatRequests::RecipientId) + .big_integer() + .not_null(), + ) + .foreign_key( + ForeignKey::create() + .name("fk-in_chat_requests-users") + .from(InChatRequests::Table, InChatRequests::RecipientId) + .to(Users::Table, Users::Id) + .on_update(ForeignKeyAction::NoAction) + .on_delete(ForeignKeyAction::Cascade), + ) + .col(ColumnDef::new(InChatRequests::Sender).string().not_null()) + .col( + ColumnDef::new(InChatRequests::InOn) + .timestamp_with_time_zone() + .not_null(), + ) + .to_owned(), + ) + .await?; + manager + .create_index( + Index::create() + .if_not_exists() + .name("sep_request") + .table(InChatRequests::Table) + .col(InChatRequests::RecipientId) + .col(InChatRequests::Sender) + .unique() + .to_owned(), + ) + .await + } +} + +#[derive(DeriveIden)] +enum InChatRequests { + Table, + Id, + RecipientId, + /// Public key of the sender + Sender, + InOn, +} diff --git a/crates/oxidetalis_migrations/src/create_outgoing_chat_requests_table.rs b/crates/oxidetalis_migrations/src/create_outgoing_chat_requests_table.rs new file mode 100644 index 0000000..4b03e99 --- /dev/null +++ b/crates/oxidetalis_migrations/src/create_outgoing_chat_requests_table.rs @@ -0,0 +1,94 @@ +// OxideTalis Messaging Protocol homeserver core implementation +// Copyright (c) 2024 OxideTalis Developers +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +use sea_orm_migration::prelude::*; + +use crate::create_users_table::Users; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_table( + Table::create() + .table(OutChatRequests::Table) + .if_not_exists() + .col( + ColumnDef::new(OutChatRequests::Id) + .big_integer() + .not_null() + .auto_increment() + .primary_key(), + ) + .col( + ColumnDef::new(OutChatRequests::SenderId) + .big_integer() + .not_null(), + ) + .foreign_key( + ForeignKey::create() + .name("fk-out_chat_requests-users") + .from(OutChatRequests::Table, OutChatRequests::SenderId) + .to(Users::Table, Users::Id) + .on_update(ForeignKeyAction::NoAction) + .on_delete(ForeignKeyAction::Cascade), + ) + .col( + ColumnDef::new(OutChatRequests::Recipient) + .string() + .not_null(), + ) + .col( + ColumnDef::new(OutChatRequests::OutOn) + .timestamp_with_time_zone() + .not_null(), + ) + .to_owned(), + ) + .await?; + + manager + .create_index( + Index::create() + .if_not_exists() + .name("sep_request") + .table(OutChatRequests::Table) + .col(OutChatRequests::SenderId) + .col(OutChatRequests::Recipient) + .unique() + .to_owned(), + ) + .await + } +} + +#[derive(DeriveIden)] +enum OutChatRequests { + Table, + Id, + SenderId, + /// Public key of the recipient + Recipient, + OutOn, +} diff --git a/crates/oxidetalis_migrations/src/create_users_status.rs b/crates/oxidetalis_migrations/src/create_users_status.rs new file mode 100644 index 0000000..6f3b571 --- /dev/null +++ b/crates/oxidetalis_migrations/src/create_users_status.rs @@ -0,0 +1,128 @@ +// OxideTalis Messaging Protocol homeserver core implementation +// Copyright (c) 2024 OxideTalis Developers +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +use std::fmt; + +use sea_orm::sea_query::extension::postgres::Type; +use sea_orm_migration::prelude::*; + +use super::create_users_table::Users; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_type( + Type::create() + .as_enum(AccessStatus::Name) + .values(vec![AccessStatus::Whitelisted, AccessStatus::Blacklisted]) + .to_owned(), + ) + .await?; + + manager + .create_table( + Table::create() + .table(UsersStatus::Table) + .if_not_exists() + .col( + ColumnDef::new(UsersStatus::Id) + .big_integer() + .not_null() + .auto_increment() + .primary_key(), + ) + .col(ColumnDef::new(UsersStatus::UserId).big_integer().not_null()) + .foreign_key( + ForeignKey::create() + .name("fk-users_status-users") + .from(UsersStatus::Table, UsersStatus::UserId) + .to(Users::Table, Users::Id) + .on_update(ForeignKeyAction::NoAction) + .on_delete(ForeignKeyAction::Cascade), + ) + .col(ColumnDef::new(UsersStatus::Target).string().not_null()) + .col( + ColumnDef::new(UsersStatus::Status) + .enumeration( + AccessStatus::Name, + [AccessStatus::Whitelisted, AccessStatus::Blacklisted], + ) + .not_null(), + ) + .col( + ColumnDef::new(UsersStatus::UpdatedAt) + .timestamp_with_time_zone() + .not_null(), + ) + .to_owned(), + ) + .await?; + + manager + .create_index( + Index::create() + .if_not_exists() + .name("sep_status") + .table(UsersStatus::Table) + .col(UsersStatus::UserId) + .col(UsersStatus::Target) + .unique() + .to_owned(), + ) + .await + } +} + +enum AccessStatus { + Name, + Whitelisted, + Blacklisted, +} + +#[derive(DeriveIden)] +enum UsersStatus { + Table, + Id, + UserId, + /// Public key of the target + Target, + Status, + UpdatedAt, +} + +impl Iden for AccessStatus { + fn unquoted(&self, s: &mut dyn fmt::Write) { + write!( + s, + "{}", + match self { + Self::Name => "access_status", + Self::Whitelisted => "whitelisted", + Self::Blacklisted => "blacklisted", + } + ) + .expect("is a string") + } +} diff --git a/crates/oxidetalis_migrations/src/create_users_table.rs b/crates/oxidetalis_migrations/src/create_users_table.rs index 71e771e..e069b89 100644 --- a/crates/oxidetalis_migrations/src/create_users_table.rs +++ b/crates/oxidetalis_migrations/src/create_users_table.rs @@ -34,7 +34,7 @@ impl MigrationTrait for Migration { .if_not_exists() .col( ColumnDef::new(Users::Id) - .integer() + .big_integer() .not_null() .auto_increment() .primary_key(), @@ -58,7 +58,7 @@ impl MigrationTrait for Migration { } #[derive(DeriveIden)] -enum Users { +pub enum Users { Table, Id, PublicKey, diff --git a/crates/oxidetalis_migrations/src/lib.rs b/crates/oxidetalis_migrations/src/lib.rs index 83f94c0..846a4ef 100644 --- a/crates/oxidetalis_migrations/src/lib.rs +++ b/crates/oxidetalis_migrations/src/lib.rs @@ -19,8 +19,12 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -pub use sea_orm_migration::prelude::*; +use sea_orm_migration::prelude::*; +pub use sea_orm_migration::MigratorTrait; +mod create_incoming_chat_requests_table; +mod create_outgoing_chat_requests_table; +mod create_users_status; mod create_users_table; pub struct Migrator; @@ -28,6 +32,11 @@ pub struct Migrator; #[async_trait::async_trait] impl MigratorTrait for Migrator { fn migrations() -> Vec> { - vec![Box::new(create_users_table::Migration)] + vec![ + Box::new(create_users_table::Migration), + Box::new(create_incoming_chat_requests_table::Migration), + Box::new(create_outgoing_chat_requests_table::Migration), + Box::new(create_users_status::Migration), + ] } }