feat: Chat request implementation #14

Manually merged
awiteb merged 55 commits from awiteb/chat-request-and-response into master 2024-07-18 14:21:39 +02:00 AGit
11 changed files with 147 additions and 106 deletions
Showing only changes of commit 2bfff6c05d - Show all commits

View file

@ -20,7 +20,7 @@ use oxidetalis_core::types::PublicKey;
use oxidetalis_entities::prelude::*; use oxidetalis_entities::prelude::*;
use sea_orm::DatabaseConnection; use sea_orm::DatabaseConnection;
use crate::errors::ApiResult; use crate::errors::ServerResult;
/// Extension trait for the `DatabaseConnection` to work with the blacklist /// Extension trait for the `DatabaseConnection` to work with the blacklist
/// table /// table
@ -31,7 +31,7 @@ pub trait BlackListExt {
&self, &self,
blacklister: &UserModel, blacklister: &UserModel,
awiteb marked this conversation as resolved
Review

ture -> true (use find and replace all, there are multiple of these)
blacklister has blacklisted

`ture` -> `true` (use find and replace all, there are multiple of these) `blacklister has blacklisted`
target_public_key: &PublicKey, target_public_key: &PublicKey,
) -> ApiResult<bool>; ) -> ServerResult<bool>;
} }
impl BlackListExt for DatabaseConnection { impl BlackListExt for DatabaseConnection {
@ -40,7 +40,7 @@ impl BlackListExt for DatabaseConnection {
&self, &self,
blacklister: &UserModel, blacklister: &UserModel,
target_public_key: &PublicKey, target_public_key: &PublicKey,
) -> ApiResult<bool> { ) -> ServerResult<bool> {
blacklister blacklister
.find_related(BlacklistEntity) .find_related(BlacklistEntity)
.filter(BlacklistColumn::Target.eq(target_public_key.to_string())) .filter(BlacklistColumn::Target.eq(target_public_key.to_string()))

View file

@ -21,7 +21,7 @@ use oxidetalis_core::types::PublicKey;
use oxidetalis_entities::prelude::*; use oxidetalis_entities::prelude::*;
use sea_orm::DatabaseConnection; use sea_orm::DatabaseConnection;
use crate::websocket::errors::{WsError, WsResult}; use crate::errors::ServerResult;
/// Extension trait for the `in_chat_requests` table. /// Extension trait for the `in_chat_requests` table.
pub trait InChatRequestsExt { pub trait InChatRequestsExt {
@ -30,7 +30,7 @@ pub trait InChatRequestsExt {
&self, &self,
requester: &PublicKey, requester: &PublicKey,
recipient: &UserModel, recipient: &UserModel,
) -> WsResult<()>; ) -> ServerResult<()>;
} }
impl InChatRequestsExt for DatabaseConnection { impl InChatRequestsExt for DatabaseConnection {
@ -39,16 +39,12 @@ impl InChatRequestsExt for DatabaseConnection {
&self, &self,
sender: &PublicKey, sender: &PublicKey,
recipient: &UserModel, recipient: &UserModel,
) -> WsResult<()> { ) -> ServerResult<()> {
if sender.to_string() == recipient.public_key {
return Err(WsError::CannotSendChatRequestToSelf);
}
if recipient if recipient
.find_related(InChatRequestsEntity) .find_related(InChatRequestsEntity)
.filter(InChatRequestsColumn::Sender.eq(sender.to_string())) .filter(InChatRequestsColumn::Sender.eq(sender.to_string()))
.one(self) .one(self)
.await .await?
.map_err(|_| WsError::InternalServerError)?
.is_none() .is_none()
awiteb marked this conversation as resolved
Review

since we are not dealing with the result. maybe a one statement approach to do

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,
    ])
    .update_column(InChatRequestsColumn::InOn)
    .to_owned(),
)
.exec(self)

here i'm updating InOn if needed, but can also be changed to do_nothing()

since we are not dealing with the result. maybe a one statement approach to do ```rust 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, ]) .update_column(InChatRequestsColumn::InOn) .to_owned(), ) .exec(self) ``` here i'm updating `InOn` if needed, but can also be changed to `do_nothing()`
{ {
InChatRequestsActiveModel { InChatRequestsActiveModel {
@ -58,8 +54,7 @@ impl InChatRequestsExt for DatabaseConnection {
..Default::default() ..Default::default()
} }
.save(self) .save(self)
.await .await?;
.map_err(|_| WsError::InternalServerError)?;
} }
Ok(()) Ok(())
} }

View file

@ -21,10 +21,7 @@ use oxidetalis_core::types::PublicKey;
use oxidetalis_entities::prelude::*; use oxidetalis_entities::prelude::*;
use sea_orm::DatabaseConnection; use sea_orm::DatabaseConnection;
use crate::{ use crate::{errors::ServerResult, websocket::errors::WsError};
errors::ApiResult,
websocket::errors::{WsError, WsResult},
};
/// Extension trait for the `out_chat_requests` table. /// Extension trait for the `out_chat_requests` table.
pub trait OutChatRequestsExt { pub trait OutChatRequestsExt {
@ -33,14 +30,14 @@ pub trait OutChatRequestsExt {
&self, &self,
requester: &UserModel, requester: &UserModel,
recipient: &PublicKey, recipient: &PublicKey,
) -> ApiResult<bool>; ) -> ServerResult<bool>;
awiteb marked this conversation as resolved
Review

should return true based on the name and description of the func

should return `true` based on the name and description of the func
Review

small nitpick (also applies to have_chat_request_to below)
I would name it get_chat_request_to, as have seems that it would be boolean

small nitpick (also applies to `have_chat_request_to` below) I would name it `get_chat_request_to`, as `have` seems that it would be boolean
/// Save the chat request in the requester table /// Save the chat request in the requester table
async fn save_out_chat_request( async fn save_out_chat_request(
&self, &self,
requester: &UserModel, requester: &UserModel,
recipient: &PublicKey, recipient: &PublicKey,
) -> WsResult<()>; ) -> ServerResult<()>;
} }
impl OutChatRequestsExt for DatabaseConnection { impl OutChatRequestsExt for DatabaseConnection {
@ -49,7 +46,7 @@ impl OutChatRequestsExt for DatabaseConnection {
&self, &self,
requester: &UserModel, requester: &UserModel,
recipient: &PublicKey, recipient: &PublicKey,
) -> ApiResult<bool> { ) -> ServerResult<bool> {
requester requester
.find_related(OutChatRequestsEntity) .find_related(OutChatRequestsEntity)
.filter(OutChatRequestsColumn::Recipient.eq(recipient.to_string())) .filter(OutChatRequestsColumn::Recipient.eq(recipient.to_string()))
@ -64,13 +61,9 @@ impl OutChatRequestsExt for DatabaseConnection {
&self, &self,
requester: &UserModel, requester: &UserModel,
recipient: &PublicKey, recipient: &PublicKey,
) -> WsResult<()> { ) -> ServerResult<()> {
if self if self.have_chat_request_to(requester, recipient).await? {
.have_chat_request_to(requester, recipient) return Err(WsError::AlreadySendChatRequest.into());
.await
.map_err(|_| WsError::InternalServerError)?
{
return Err(WsError::AlreadySendChatRequest);
} }
OutChatRequestsActiveModel { OutChatRequestsActiveModel {
sender_id: Set(requester.id), sender_id: Set(requester.id),
@ -79,8 +72,7 @@ impl OutChatRequestsExt for DatabaseConnection {
..Default::default() ..Default::default()
} }
.save(self) .save(self)
.await .await?;
.map_err(|_| WsError::InternalServerError)?;
Ok(()) Ok(())
} }
} }

View file

@ -21,29 +21,29 @@ use oxidetalis_core::types::PublicKey;
use oxidetalis_entities::prelude::*; use oxidetalis_entities::prelude::*;
use sea_orm::DatabaseConnection; use sea_orm::DatabaseConnection;
use crate::errors::{ApiError, ApiResult}; use crate::{errors::ServerResult, routes::ApiError};
pub trait UserTableExt { pub trait UserTableExt {
/// Returns true if there is users in the database /// Returns true if there is users in the database
async fn users_exists_in_database(&self) -> ApiResult<bool>; async fn users_exists_in_database(&self) -> ServerResult<bool>;
/// Register new user /// 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 /// Returns user by its public key
async fn get_user_by_pubk(&self, public_key: &PublicKey) -> ApiResult<Option<UserModel>>; async fn get_user_by_pubk(&self, public_key: &PublicKey) -> ServerResult<Option<UserModel>>;
} }
impl UserTableExt for DatabaseConnection { impl UserTableExt for DatabaseConnection {
#[logcall] #[logcall]
async fn users_exists_in_database(&self) -> ApiResult<bool> { async fn users_exists_in_database(&self) -> ServerResult<bool> {
UserEntity::find() UserEntity::find()
.one(self) .one(self)
.await .await
.map_err(Into::into)
.map(|u| u.is_some()) .map(|u| u.is_some())
.map_err(Into::into)
} }
#[logcall] #[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 { if let Err(err) = (UserActiveModel {
public_key: Set(public_key.to_string()), public_key: Set(public_key.to_string()),
is_admin: Set(is_admin), is_admin: Set(is_admin),
@ -53,7 +53,7 @@ impl UserTableExt for DatabaseConnection {
.await .await
{ {
if let Some(SqlErr::UniqueConstraintViolation(_)) = err.sql_err() { if let Some(SqlErr::UniqueConstraintViolation(_)) = err.sql_err() {
return Err(ApiError::AlreadyRegistered); return Err(ApiError::AlreadyRegistered.into());
} }
} }
@ -61,11 +61,11 @@ impl UserTableExt for DatabaseConnection {
} }
#[logcall] #[logcall]
async fn get_user_by_pubk(&self, public_key: &PublicKey) -> ApiResult<Option<UserModel>> { async fn get_user_by_pubk(&self, public_key: &PublicKey) -> ServerResult<Option<UserModel>> {
UserEntity::find() UserEntity::find()
.filter(UserColumn::PublicKey.eq(public_key.to_string())) .filter(UserColumn::PublicKey.eq(public_key.to_string()))
.one(self) .one(self)
.await .await
.map_err(ApiError::SeaOrm) .map_err(Into::into)
} }
} }

View file

@ -21,10 +21,7 @@ use oxidetalis_core::types::PublicKey;
use oxidetalis_entities::prelude::*; use oxidetalis_entities::prelude::*;
use sea_orm::DatabaseConnection; use sea_orm::DatabaseConnection;
use crate::{ use crate::{errors::ServerResult, websocket::errors::WsError};
errors::ApiResult,
websocket::errors::{WsError, WsResult},
};
/// Extension trait for the `DatabaseConnection` to work with the whitelist /// Extension trait for the `DatabaseConnection` to work with the whitelist
/// table /// table
@ -35,14 +32,14 @@ pub trait WhiteListExt {
&self, &self,
awiteb marked this conversation as resolved
Review

ture
The whitelister has whitelisted

ture The whitelister has whitelisted
whitelister: &UserModel, whitelister: &UserModel,
target_public_key: &PublicKey, target_public_key: &PublicKey,
) -> ApiResult<bool>; ) -> ServerResult<bool>;
/// Add the `target_public_key` to the whitelist of the `whitelister` /// Add the `target_public_key` to the whitelist of the `whitelister`
async fn add_to_whitelist( async fn add_to_whitelist(
&self, &self,
whitelister: &UserModel, whitelister: &UserModel,
target_public_key: &PublicKey, target_public_key: &PublicKey,
) -> WsResult<()>; ) -> ServerResult<()>;
} }
impl WhiteListExt for DatabaseConnection { impl WhiteListExt for DatabaseConnection {
@ -50,7 +47,7 @@ impl WhiteListExt for DatabaseConnection {
&self, &self,
whitelister: &UserModel, whitelister: &UserModel,
target_public_key: &PublicKey, target_public_key: &PublicKey,
) -> ApiResult<bool> { ) -> ServerResult<bool> {
whitelister whitelister
.find_related(WhitelistEntity) .find_related(WhitelistEntity)
.filter(WhitelistColumn::Target.eq(target_public_key.to_string())) .filter(WhitelistColumn::Target.eq(target_public_key.to_string()))
@ -64,16 +61,12 @@ impl WhiteListExt for DatabaseConnection {
&self, &self,
whitelister: &UserModel, whitelister: &UserModel,
target_public_key: &PublicKey, target_public_key: &PublicKey,
) -> WsResult<()> { ) -> ServerResult<()> {
if self if self.is_whitelisted(whitelister, target_public_key).await? {
.is_whitelisted(whitelister, target_public_key) return Err(WsError::AlreadyOnTheWhitelist.into());
.await
.map_err(|_| WsError::InternalServerError)?
{
return Err(WsError::AlreadyOnTheWhitelist);
} }
if whitelister.public_key == target_public_key.to_string() { if whitelister.public_key == target_public_key.to_string() {
return Err(WsError::CannotAddSelfToWhitelist); return Err(WsError::CannotAddSelfToWhitelist.into());
} }
WhitelistActiveModel { WhitelistActiveModel {
user_id: Set(whitelister.id), user_id: Set(whitelister.id),
@ -82,8 +75,7 @@ impl WhiteListExt for DatabaseConnection {
..Default::default() ..Default::default()
} }
.save(self) .save(self)
.await .await?;
.map_err(|_| WsError::InternalServerError)?;
Ok(()) Ok(())
} }
} }

View file

@ -14,24 +14,16 @@
// You should have received a copy of the GNU Affero General Public License // You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://gnu.org/licenses/agpl-3.0>. // along with this program. If not, see <https://gnu.org/licenses/agpl-3.0>.
use salvo::{ use sea_orm::DbErr;
http::StatusCode,
oapi::{Components as OapiComponents, EndpointOutRegister, Operation as OapiOperation},
Response,
Scribe,
};
use crate::{routes::write_json_body, schemas::MessageSchema}; use crate::{routes::ApiError, websocket::errors::WsError};
/// Result type of the homeserver /// Result type of the homeserver
#[allow(clippy::absolute_paths)] pub(crate) type ServerResult<T> = Result<T, ServerError>;
pub(crate) type Result<T> = std::result::Result<T, Error>;
#[allow(clippy::absolute_paths)]
pub type ApiResult<T> = std::result::Result<T, ApiError>;
/// The homeserver errors /// The homeserver errors
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub(crate) enum Error { pub enum InternalError {
#[error("Database Error: {0}")] #[error("Database Error: {0}")]
Database(#[from] sea_orm::DbErr), Database(#[from] sea_orm::DbErr),
#[error("{0}")] #[error("{0}")]
@ -39,43 +31,39 @@ pub(crate) enum Error {
} }
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum ApiError { /// The homeserver errors
/// Error from the database (500 Internal Server Error) pub enum ServerError {
#[error("Internal server error")] /// Internal server error, should not be exposed to the user
SeaOrm(#[from] sea_orm::DbErr), #[error("{0}")]
/// The server registration is closed (403 Forbidden) Internal(#[from] InternalError),
#[error("Server registration is closed")] /// API error, errors happening in the API
RegistrationClosed, #[error("{0}")]
/// The entered public key is already registered (400 Bad Request) Api(#[from] ApiError),
#[error("The entered public key is already registered")] /// WebSocket error, errors happening in the WebSocket
AlreadyRegistered, #[error("{0}")]
/// The user entered two different public keys Ws(#[from] WsError),
/// one in the header and other in the request body
/// (400 Bad Request)
#[error("You entered two different public keys")]
TwoDifferentKeys,
} }
impl ApiError { impl From<DbErr> for ServerError {
/// Status code of the error fn from(err: DbErr) -> Self {
pub const fn status_code(&self) -> StatusCode { Self::Internal(err.into())
match self { }
Self::SeaOrm(_) => StatusCode::INTERNAL_SERVER_ERROR, }
Self::RegistrationClosed => StatusCode::FORBIDDEN,
Self::AlreadyRegistered | Self::TwoDifferentKeys => StatusCode::BAD_REQUEST, impl From<ServerError> for WsError {
fn from(err: ServerError) -> Self {
match err {
ServerError::Internal(_) | ServerError::Api(_) => WsError::InternalServerError,
ServerError::Ws(err) => err,
} }
} }
} }
impl EndpointOutRegister for ApiError { impl From<ServerError> for ApiError {
fn register(_: &mut OapiComponents, _: &mut OapiOperation) {} fn from(err: ServerError) -> Self {
} match err {
ServerError::Internal(_) | ServerError::Ws(_) => ApiError::Internal,
impl Scribe for ApiError { ServerError::Api(err) => err,
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()));
} }
} }

View file

@ -39,9 +39,9 @@ macro_rules! try_ws {
match $result_expr { match $result_expr {
Ok(val) => val, Ok(val) => val,
Err(err) => { Err(err) => {
log::error!("Error in try_ws macro: {err:?}"); log::error!("{err}");
return $crate::websocket::ServerEvent::<$crate::websocket::Unsigned>::from( return $crate::websocket::ServerEvent::<$crate::websocket::Unsigned>::from(
$crate::websocket::errors::WsError::InternalServerError, $crate::websocket::errors::WsError::from(err),
); );
} }
} }
@ -60,11 +60,12 @@ macro_rules! try_ws {
#[macro_export] #[macro_export]
macro_rules! ws_errors { macro_rules! ws_errors {
($($name:ident = $reason:tt),+ $(,)?) => { ($($name:ident = $reason:tt),+ $(,)?) => {
#[derive(Debug)] #[derive(Debug, thiserror::Error)]
#[doc = "Websocket errors, returned in the websocket communication"] #[doc = "Websocket errors, returned in the websocket communication"]
pub enum WsError { pub enum WsError {
$( $(
#[doc = $reason] #[doc = $reason]
#[error($reason)]
$name $name
),+ ),+
} }

View file

@ -19,6 +19,7 @@
use std::process::ExitCode; use std::process::ExitCode;
use errors::ServerError;
use oxidetalis_config::{CliArgs, Parser}; use oxidetalis_config::{CliArgs, Parser};
use oxidetalis_migrations::MigratorTrait; use oxidetalis_migrations::MigratorTrait;
use salvo::{conn::TcpListener, Listener, Server}; use salvo::{conn::TcpListener, Listener, Server};
@ -34,11 +35,12 @@ mod schemas;
mod utils; mod utils;
mod websocket; mod websocket;
async fn try_main() -> errors::Result<()> { async fn try_main() -> errors::ServerResult<()> {
pretty_env_logger::init_timed(); pretty_env_logger::init_timed();
log::info!("Parsing configuration"); 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!("Configuration parsed successfully");
log::info!("Connecting to the database"); log::info!("Connecting to the database");
let connection = sea_orm::Database::connect(utils::postgres_url(&config.postgresdb)).await?; let connection = sea_orm::Database::connect(utils::postgres_url(&config.postgresdb)).await?;

View file

@ -0,0 +1,68 @@
// OxideTalis Messaging Protocol homeserver implementation
// Copyright (C) 2024 OxideTalis Developers <otmp@4rs.nl>
//
// 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 <https://gnu.org/licenses/agpl-3.0>.
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<T> = Result<T, ApiError>;
#[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,
}
impl ApiError {
/// Status code of the error
pub const fn status_code(&self) -> StatusCode {
match self {
Self::Internal => StatusCode::INTERNAL_SERVER_ERROR,
Self::RegistrationClosed => StatusCode::FORBIDDEN,
Self::AlreadyRegistered | Self::TwoDifferentKeys => 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()));
}
}

View file

@ -27,8 +27,11 @@ use crate::nonce::NonceCache;
use crate::schemas::MessageSchema; use crate::schemas::MessageSchema;
use crate::{middlewares, websocket}; use crate::{middlewares, websocket};
mod errors;
mod user; mod user;
pub use errors::*;
pub fn write_json_body(res: &mut Response, json_body: impl serde::Serialize) { 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")) res.write_body(serde_json::to_string(&json_body).expect("Json serialization can't be fail"))
.ok(); .ok();

View file

@ -26,9 +26,9 @@ use salvo::{
Writer, Writer,
}; };
use super::{ApiError, ApiResult};
use crate::{ use crate::{
database::UserTableExt, database::UserTableExt,
errors::{ApiError, ApiResult},
extensions::DepotExt, extensions::DepotExt,
middlewares, middlewares,
schemas::{EmptySchema, MessageSchema, RegisterUserBody}, schemas::{EmptySchema, MessageSchema, RegisterUserBody},