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 sea_orm::DatabaseConnection;
use crate::errors::ApiResult;
use crate::errors::ServerResult;
/// Extension trait for the `DatabaseConnection` to work with the blacklist
/// table
@ -31,7 +31,7 @@ pub trait BlackListExt {
&self,
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,
) -> ApiResult<bool>;
) -> ServerResult<bool>;
}
impl BlackListExt for DatabaseConnection {
@ -40,7 +40,7 @@ impl BlackListExt for DatabaseConnection {
&self,
blacklister: &UserModel,
target_public_key: &PublicKey,
) -> ApiResult<bool> {
) -> ServerResult<bool> {
blacklister
.find_related(BlacklistEntity)
.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 sea_orm::DatabaseConnection;
use crate::websocket::errors::{WsError, WsResult};
use crate::errors::ServerResult;
/// Extension trait for the `in_chat_requests` table.
pub trait InChatRequestsExt {
@ -30,7 +30,7 @@ pub trait InChatRequestsExt {
&self,
requester: &PublicKey,
recipient: &UserModel,
) -> WsResult<()>;
) -> ServerResult<()>;
}
impl InChatRequestsExt for DatabaseConnection {
@ -39,16 +39,12 @@ impl InChatRequestsExt for DatabaseConnection {
&self,
sender: &PublicKey,
recipient: &UserModel,
) -> WsResult<()> {
if sender.to_string() == recipient.public_key {
return Err(WsError::CannotSendChatRequestToSelf);
}
) -> ServerResult<()> {
if recipient
.find_related(InChatRequestsEntity)
.filter(InChatRequestsColumn::Sender.eq(sender.to_string()))
.one(self)
.await
.map_err(|_| WsError::InternalServerError)?
.await?
.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 {
@ -58,8 +54,7 @@ impl InChatRequestsExt for DatabaseConnection {
..Default::default()
}
.save(self)
.await
.map_err(|_| WsError::InternalServerError)?;
.await?;
}
Ok(())
}

View file

@ -21,10 +21,7 @@ use oxidetalis_core::types::PublicKey;
use oxidetalis_entities::prelude::*;
use sea_orm::DatabaseConnection;
use crate::{
errors::ApiResult,
websocket::errors::{WsError, WsResult},
};
use crate::{errors::ServerResult, websocket::errors::WsError};
/// Extension trait for the `out_chat_requests` table.
pub trait OutChatRequestsExt {
@ -33,14 +30,14 @@ pub trait OutChatRequestsExt {
&self,
requester: &UserModel,
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
async fn save_out_chat_request(
&self,
requester: &UserModel,
recipient: &PublicKey,
) -> WsResult<()>;
) -> ServerResult<()>;
}
impl OutChatRequestsExt for DatabaseConnection {
@ -49,7 +46,7 @@ impl OutChatRequestsExt for DatabaseConnection {
&self,
requester: &UserModel,
recipient: &PublicKey,
) -> ApiResult<bool> {
) -> ServerResult<bool> {
requester
.find_related(OutChatRequestsEntity)
.filter(OutChatRequestsColumn::Recipient.eq(recipient.to_string()))
@ -64,13 +61,9 @@ impl OutChatRequestsExt for DatabaseConnection {
&self,
requester: &UserModel,
recipient: &PublicKey,
) -> WsResult<()> {
if self
.have_chat_request_to(requester, recipient)
.await
.map_err(|_| WsError::InternalServerError)?
{
return Err(WsError::AlreadySendChatRequest);
) -> ServerResult<()> {
if self.have_chat_request_to(requester, recipient).await? {
return Err(WsError::AlreadySendChatRequest.into());
}
OutChatRequestsActiveModel {
sender_id: Set(requester.id),
@ -79,8 +72,7 @@ impl OutChatRequestsExt for DatabaseConnection {
..Default::default()
}
.save(self)
.await
.map_err(|_| WsError::InternalServerError)?;
.await?;
Ok(())
}
}

View file

@ -21,29 +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<bool>;
async fn users_exists_in_database(&self) -> ServerResult<bool>;
/// 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) -> ApiResult<Option<UserModel>>;
async fn get_user_by_pubk(&self, public_key: &PublicKey) -> ServerResult<Option<UserModel>>;
}
impl UserTableExt for DatabaseConnection {
#[logcall]
async fn users_exists_in_database(&self) -> ApiResult<bool> {
async fn users_exists_in_database(&self) -> ServerResult<bool> {
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),
@ -53,7 +53,7 @@ impl UserTableExt for DatabaseConnection {
.await
{
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]
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()
.filter(UserColumn::PublicKey.eq(public_key.to_string()))
.one(self)
.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 sea_orm::DatabaseConnection;
use crate::{
errors::ApiResult,
websocket::errors::{WsError, WsResult},
};
use crate::{errors::ServerResult, websocket::errors::WsError};
/// Extension trait for the `DatabaseConnection` to work with the whitelist
/// table
@ -35,14 +32,14 @@ pub trait WhiteListExt {
&self,
awiteb marked this conversation as resolved
Review

ture
The whitelister has whitelisted

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

View file

@ -14,24 +14,16 @@
// 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 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<T> = std::result::Result<T, Error>;
#[allow(clippy::absolute_paths)]
pub type ApiResult<T> = std::result::Result<T, ApiError>;
pub(crate) type ServerResult<T> = Result<T, ServerError>;
/// 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,39 @@ 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")]
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,
/// 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::AlreadyRegistered | Self::TwoDifferentKeys => StatusCode::BAD_REQUEST,
impl From<DbErr> for ServerError {
fn from(err: DbErr) -> Self {
Self::Internal(err.into())
}
}
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 {
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<ServerError> for ApiError {
fn from(err: ServerError) -> Self {
match err {
ServerError::Internal(_) | ServerError::Ws(_) => ApiError::Internal,
ServerError::Api(err) => err,
}
}
}

View file

@ -39,9 +39,9 @@ macro_rules! try_ws {
match $result_expr {
Ok(val) => val,
Err(err) => {
log::error!("Error in try_ws macro: {err:?}");
log::error!("{err}");
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_rules! ws_errors {
($($name:ident = $reason:tt),+ $(,)?) => {
#[derive(Debug)]
#[derive(Debug, thiserror::Error)]
#[doc = "Websocket errors, returned in the websocket communication"]
pub enum WsError {
$(
#[doc = $reason]
#[error($reason)]
$name
),+
}

View file

@ -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};
@ -34,11 +35,12 @@ 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?;

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::{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();

View file

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