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
34 changed files with 1860 additions and 122 deletions

1
Cargo.lock generated
View file

@ -1965,6 +1965,7 @@ dependencies = [
name = "oxidetalis_entities" name = "oxidetalis_entities"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"chrono",
"sea-orm", "sea-orm",
] ]

View file

@ -0,0 +1,61 @@
// 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>.
//! 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()
})
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()`
.on_conflict(
OnConflict::columns([
InChatRequestsColumn::RecipientId,
InChatRequestsColumn::Sender,
])
.do_nothing()
.to_owned(),
)
.exec(self)
.await?;
Ok(())
}
}

View file

@ -16,6 +16,12 @@
//! Database utilities for the OxideTalis homeserver. //! Database utilities for the OxideTalis homeserver.
mod in_chat_requests;
mod out_chat_requests;
mod user; mod user;
mod user_status;
pub use in_chat_requests::*;
pub use out_chat_requests::*;
pub use user::*; pub use user::*;
pub use user_status::*;

View file

@ -0,0 +1,102 @@
// 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>.
//! 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,
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
) -> ServerResult<Option<OutChatRequestsModel>>;
/// 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<Option<OutChatRequestsModel>> {
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(())
}
}

View file

@ -21,27 +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
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),
@ -51,10 +53,19 @@ 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::DuplicatedUser); return Err(ApiError::AlreadyRegistered.into());
} }
} }
Ok(()) Ok(())
} }
#[logcall]
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(Into::into)
}
} }

View file

@ -0,0 +1,306 @@
// 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>.
//! 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<bool>;
/// Returns true if the `blacklister` has blacklisted the
/// `target_public_key`
async fn is_blacklisted(
&self,
blacklister: &UserModel,
target_public_key: &PublicKey,
) -> ServerResult<bool>;
/// 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<Vec<UsersStatusModel>>;
/// Returns the blacklist of the user
async fn user_blacklist(
&self,
blacklister: &UserModel,
page: NonZeroU32,
page_size: NonZeroU8,
) -> ServerResult<Vec<UsersStatusModel>>;
}
impl UsersStatusExt for DatabaseConnection {
#[logcall::logcall]
async fn is_whitelisted(
&self,
whitelister: &UserModel,
target_public_key: &PublicKey,
) -> ServerResult<bool> {
awiteb marked this conversation as resolved
Review

you can use get_user_status here and also is_blacklisted. also in remove... functions

you can use `get_user_status` here and also `is_blacklisted`. also in `remove...` functions
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<bool> {
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<Vec<UsersStatusModel>> {
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<Vec<UsersStatusModel>> {
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<Option<UsersStatusModel>> {
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)
}

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,41 @@ 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
DuplicatedUser, #[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::DuplicatedUser | Self::TwoDifferentKeys => StatusCode::BAD_REQUEST, impl From<ServerError> 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 { impl From<ServerError> for ApiError {
fn register(_: &mut OapiComponents, _: &mut OapiOperation) {} fn from(err: ServerError) -> Self {
} match err {
ServerError::Ws(WsError::RegistredUserEvent) => ApiError::NotRegisteredUser,
impl Scribe for ApiError { ServerError::Internal(_) | ServerError::Ws(_) => ApiError::Internal,
fn render(self, res: &mut Response) { ServerError::Api(err) => err,
log::error!("Error: {self}"); }
res.status_code(self.status_code());
write_json_body(res, MessageSchema::new(self.to_string()));
} }
} }

View file

@ -18,20 +18,21 @@ use std::sync::Arc;
use chrono::Utc; use chrono::Utc;
use oxidetalis_config::Config; use oxidetalis_config::Config;
use oxidetalis_core::types::PublicKey;
use rayon::iter::{IntoParallelRefMutIterator, ParallelIterator}; use rayon::iter::{IntoParallelRefMutIterator, ParallelIterator};
use salvo::{websocket::Message, Depot}; use salvo::Depot;
use sea_orm::DatabaseConnection; use sea_orm::DatabaseConnection;
use uuid::Uuid; use uuid::Uuid;
use crate::{ use crate::{
nonce::NonceCache, nonce::NonceCache,
websocket::{OnlineUsers, ServerEvent, SocketUserData}, websocket::{OnlineUsers, ServerEvent, SocketUserData, Unsigned},
}; };
/// Extension trait for the Depot. /// Extension trait for the Depot.
pub trait DepotExt { pub trait DepotExt {
/// Returns the database connection /// Returns the database connection
fn db_conn(&self) -> &DatabaseConnection; fn db_conn(&self) -> Arc<DatabaseConnection>;
/// Returns the server configuration /// Returns the server configuration
fn config(&self) -> &Config; fn config(&self) -> &Config;
/// Retutns the nonce cache /// Retutns the nonce cache
@ -54,12 +55,20 @@ pub trait OnlineUsersExt {
/// Disconnect inactive users (who not respond for the ping event) /// Disconnect inactive users (who not respond for the ping event)
async fn disconnect_inactive_users(&self); 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<Uuid>;
/// Send an event to user by connection id
async fn send(&self, conn_id: &Uuid, event: ServerEvent<Unsigned>);
} }
impl DepotExt for Depot { impl DepotExt for Depot {
fn db_conn(&self) -> &DatabaseConnection { fn db_conn(&self) -> Arc<DatabaseConnection> {
Arc::clone(
self.obtain::<Arc<DatabaseConnection>>() self.obtain::<Arc<DatabaseConnection>>()
.expect("Database connection not found") .expect("Database connection not found"),
)
} }
fn config(&self) -> &Config { fn config(&self) -> &Config {
@ -87,9 +96,10 @@ impl OnlineUsersExt for OnlineUsers {
let now = Utc::now(); let now = Utc::now();
self.write().await.par_iter_mut().for_each(|(_, u)| { self.write().await.par_iter_mut().for_each(|(_, u)| {
u.pinged_at = now; u.pinged_at = now;
let _ = u.sender.unbounded_send(Ok(Message::from( let _ = u.sender.unbounded_send(Ok(ServerEvent::ping()
&ServerEvent::ping().sign(&u.shared_secret), .sign(&u.shared_secret)
))); .as_ref()
.into()));
}); });
} }
@ -110,4 +120,20 @@ impl OnlineUsersExt for OnlineUsers {
true true
}); });
} }
async fn is_online(&self, public_key: &PublicKey) -> Option<Uuid> {
self.read()
.await
.iter()
.find(|(_, u)| &u.public_key == public_key)
.map(|(c, _)| *c)
}
async fn send(&self, conn_id: &Uuid, event: ServerEvent<Unsigned>) {
if let Some(user) = self.read().await.get(conn_id) {
awiteb marked this conversation as resolved
Review

use get

if let Some(user) = self.read().await.get(conn_id) {
// ...
}
use `get` ```rust if let Some(user) = self.read().await.get(conn_id) { // ... } ```
let _ = user
.sender
.unbounded_send(Ok(event.sign(&user.shared_secret).as_ref().into()));
}
}
} }

View file

@ -0,0 +1,93 @@
// 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>.
//! 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
),+
}
}
}
};
}

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};
@ -26,18 +27,21 @@ use salvo::{conn::TcpListener, Listener, Server};
mod database; mod database;
mod errors; mod errors;
mod extensions; mod extensions;
mod macros;
mod middlewares; mod middlewares;
mod nonce; mod nonce;
mod parameters;
mod routes; mod routes;
mod schemas; 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,21 @@
// 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>.
//! Set of route parameters for the API
mod pagination;
pub use pagination::*;

View file

@ -0,0 +1,131 @@
// 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>.
//! 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};
awiteb marked this conversation as resolved
Review

We can add EndpointArgRegister implementation and thus we can use it in the arg directly

async fn user_whitelist(
    req: &mut Request,
    depot: &mut Depot,
    pagination: Pagination,
) -> ApiResult<Json<Vec<WhiteListedUser>>>
impl EndpointArgRegister for Pagination {
    fn register(components: &mut OapiComponents, operation: &mut Operation, _arg: &str) {
        for parameter in Self::to_parameters(components) {
            operation.parameters.insert(parameter);
        }
    }
}

If we go with this, we can also use ToParameters macro to generate the implementation.
The issue with it is that it doesn't use ApiError, but if we are using it as a paremeter, salvo will handle the error reporting

We can add `EndpointArgRegister` implementation and thus we can use it in the arg directly ```rust async fn user_whitelist( req: &mut Request, depot: &mut Depot, pagination: Pagination, ) -> ApiResult<Json<Vec<WhiteListedUser>>> ``` ```rust impl EndpointArgRegister for Pagination { fn register(components: &mut OapiComponents, operation: &mut Operation, _arg: &str) { for parameter in Self::to_parameters(components) { operation.parameters.insert(parameter); } } } ``` If we go with this, we can also use `ToParameters` macro to generate the implementation. The issue with it is that it doesn't use `ApiError`, but if we are using it as a paremeter, `salvo` will handle the error reporting
Review

I'll see it, the reason why I don't put it as an argument, is because
Salvo ask me to implement ToSchema to it.

As I see, Salvo want it as one parameter, but the pagination
is two parameters. I'll see

I'll see it, the reason why I don't put it as an argument, is because Salvo ask me to implement `ToSchema` to it. As I see, Salvo want it as one parameter, but the pagination is two parameters. I'll see
Review

Ok, I get it. It ask me to implement ToSchema because of this

impl<T, const R: bool> EndpointArgRegister for QueryParam<T, R>
where
    T: ToSchema, // <----
{
    fn register(components: &mut Components, operation: &mut Operation, arg: &str) {
        let parameter = Parameter::new(arg)
            .parameter_in(ParameterIn::Query)
            .description(format!("Get parameter `{arg}` from request url query."))
            .schema(T::to_schema(components))
            .required(R);
        operation.parameters.insert(parameter);
    }
}

So I can just implement the EndpointArgRegister trait and not using QueryParam.

Thank you @Amjad50

Ok, I get it. It ask me to implement `ToSchema` because of this ```rust impl<T, const R: bool> EndpointArgRegister for QueryParam<T, R> where T: ToSchema, // <---- { fn register(components: &mut Components, operation: &mut Operation, arg: &str) { let parameter = Parameter::new(arg) .parameter_in(ParameterIn::Query) .description(format!("Get parameter `{arg}` from request url query.")) .schema(T::to_schema(components)) .required(R); operation.parameters.insert(parameter); } } ``` So I can just implement the `EndpointArgRegister` trait and not using `QueryParam`. Thank you @Amjad50
#[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<Self> {
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> {
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<T: FromStr>(req: &Request, name: &str, default_value: T) -> ApiResult<T>
where
<T as FromStr>::Err: fmt::Display,
{
Ok(req
.queries()
.get(name)
.map(|p| p.parse::<T>())
.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),
)
}

View file

@ -0,0 +1,77 @@
// 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,
/// 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()));
}
}

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

@ -20,18 +20,20 @@ use oxidetalis_core::types::{PublicKey, Signature};
use salvo::{ use salvo::{
http::StatusCode, http::StatusCode,
oapi::{endpoint, extract::JsonBody}, oapi::{endpoint, extract::JsonBody},
writing::Json,
Depot, Depot,
Request, Request,
Router, Router,
Writer, Writer,
}; };
use super::{ApiError, ApiResult};
use crate::{ use crate::{
database::UserTableExt, database::{UserTableExt, UsersStatusExt},
errors::{ApiError, ApiResult},
extensions::DepotExt, extensions::DepotExt,
middlewares, middlewares,
schemas::{EmptySchema, MessageSchema, RegisterUserBody}, parameters::Pagination,
schemas::{BlackListedUser, EmptySchema, MessageSchema, RegisterUserBody, WhiteListedUser},
utils, utils,
}; };
@ -79,10 +81,90 @@ pub async fn register(
Ok(EmptySchema::new(StatusCode::CREATED)) 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<WhiteListedUser>),
(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<Json<Vec<WhiteListedUser>>> {
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<BlackListedUser>),
(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<Json<Vec<BlackListedUser>>> {
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 /// The route of the endpoints of this module
pub fn route() -> Router { pub fn route() -> Router {
Router::new() Router::new()
.push(Router::with_path("register").post(register)) .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::public_key_check)
.hoop(middlewares::signature_check) .hoop(middlewares::signature_check)
} }

View file

@ -14,7 +14,11 @@
// 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 std::str::FromStr;
use chrono::{DateTime, Utc};
use oxidetalis_core::{cipher::K256Secret, types::PublicKey}; use oxidetalis_core::{cipher::K256Secret, types::PublicKey};
use oxidetalis_entities::prelude::*;
use salvo::oapi::ToSchema; use salvo::oapi::ToSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -25,3 +29,57 @@ pub struct RegisterUserBody {
/// The public key of the user /// The public key of the user
pub public_key: PublicKey, 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<Utc>,
}
#[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<Utc>,
}
impl Default for WhiteListedUser {
fn default() -> Self {
WhiteListedUser::new(
PublicKey::from_str("bYhbrm61ov8GLZfskUYbsCLJTfaacMsuTBYgBABEH9dy").expect("is valid"),
Utc::now(),
)
}
}
impl From<UsersStatusModel> 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<UsersStatusModel> 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,
}
}
}

View file

@ -16,42 +16,27 @@
//! Websocket errors //! Websocket errors
use crate::ws_errors;
/// Result type of websocket /// Result type of websocket
pub type WsResult<T> = Result<T, WsError>; pub type WsResult<T> = Result<T, WsError>;
/// Websocket errors, returned in the websocket communication ws_errors! {
#[derive(Debug)] InternalServerError = "Internal server error",
pub enum WsError { InvalidSignature = "Invalid event signature",
/// The signature is invalid NotTextMessage = "The websocket message must be text message",
InvalidSignature, InvalidJsonData = "Received invalid json data, the text must be valid json",
/// Message type must be text UnknownClientEvent = "Unknown client event, the event is not recognized by the server",
NotTextMessage, RegistredUserEvent = "The event is only for registred users",
/// Invalid json data UserNotFound = "The user is not registered in the server",
InvalidJsonData, AlreadyOnTheWhitelist = "The user is already on your whitelist",
/// Unknown client event CannotAddSelfToWhitelist = "You cannot add yourself to the whitelist",
UnknownClientEvent, 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",
impl WsError { CannotSendChatRequestToSelf = "You cannot send a chat request to yourself",
/// Returns error name CannotRespondToOwnChatRequest = "You cannot respond to your own chat request",
pub const fn name(&self) -> &'static str { NoChatRequestFromRecipient = "You do not have a chat request from the recipient",
match self { RecipientBlacklist = "You cannot send a chat request because you are on the recipient's blacklist.",
WsError::InvalidSignature => "InvalidSignature", AlreadyInRecipientWhitelist = "You are already on the recipient's whitelist and can chat with them."
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"
}
}
}
} }

View file

@ -16,7 +16,7 @@
//! Events that the client send it //! Events that the client send it
use oxidetalis_core::types::Signature; use oxidetalis_core::types::{PublicKey, Signature};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{nonce::NonceCache, utils}; use crate::{nonce::NonceCache, utils};
@ -24,10 +24,14 @@ use crate::{nonce::NonceCache, utils};
/// Client websocket event /// Client websocket event
#[derive(Deserialize, Clone, Debug)] #[derive(Deserialize, Clone, Debug)]
pub struct ClientEvent { pub struct ClientEvent {
#[serde(flatten)]
pub event: ClientEventType, pub event: ClientEventType,
signature: Signature, signature: Signature,
} }
// ## Important for contuributors
// Please make sure to order the event data alphabetically.
/// Client websocket event type /// Client websocket event type
#[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Debug)] #[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Debug)]
#[serde(rename_all = "PascalCase", tag = "event", content = "data")] #[serde(rename_all = "PascalCase", tag = "event", content = "data")]
@ -36,6 +40,10 @@ pub enum ClientEventType {
Ping { timestamp: u64 }, Ping { timestamp: u64 },
/// Pong event /// Pong event
Pong { timestamp: u64 }, Pong { timestamp: u64 },
/// Request to chat with a user
ChatRequest { to: PublicKey },
/// Response to a chat request
ChatRequestResponse { accepted: bool, to: PublicKey },
} }
impl ClientEventType { impl ClientEventType {

View file

@ -19,7 +19,10 @@
use std::marker::PhantomData; use std::marker::PhantomData;
use chrono::Utc; use chrono::Utc;
use oxidetalis_core::{cipher::K256Secret, types::Signature}; use oxidetalis_core::{
cipher::K256Secret,
types::{PublicKey, Signature},
};
use salvo::websocket::Message; use salvo::websocket::Message;
use serde::Serialize; use serde::Serialize;
@ -28,6 +31,7 @@ use crate::websocket::errors::WsError;
/// Signed marker, used to indicate that the event is signed /// Signed marker, used to indicate that the event is signed
pub struct Signed; pub struct Signed;
/// Unsigned marker, used to indicate that the event is unsigned /// Unsigned marker, used to indicate that the event is unsigned
#[derive(Debug)]
pub struct Unsigned; pub struct Unsigned;
/// Server websocket event /// Server websocket event
@ -42,12 +46,16 @@ pub struct ServerEvent<T> {
/// server websocket event type /// server websocket event type
#[derive(Serialize, Clone, Eq, PartialEq, Debug)] #[derive(Serialize, Clone, Eq, PartialEq, Debug)]
#[serde(rename_all = "PascalCase")] #[serde(rename_all = "PascalCase", tag = "event", content = "data")]
pub enum ServerEventType { pub enum ServerEventType {
/// Ping event /// Ping event
Ping { timestamp: u64 }, Ping { timestamp: u64 },
/// Pong event /// Pong event
Pong { timestamp: u64 }, 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 event
awiteb marked this conversation as resolved
Review

in the client, you mentioned to make it alphabetical, should it be the same here?

in the client, you mentioned to make it alphabetical, should it be the same here?
Review

Yes, I forgot it

Yes, I forgot it
Error { Error {
name: &'static str, name: &'static str,
@ -88,6 +96,16 @@ impl ServerEvent<Unsigned> {
}) })
} }
/// 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 /// Sign the event
pub fn sign(self, shared_secret: &[u8; 32]) -> ServerEvent<Signed> { pub fn sign(self, shared_secret: &[u8; 32]) -> ServerEvent<Signed> {
ServerEvent::<Signed> { ServerEvent::<Signed> {
@ -101,6 +119,12 @@ impl ServerEvent<Unsigned> {
} }
} }
impl<S> AsRef<Self> for ServerEvent<S> {
fn as_ref(&self) -> &Self {
self
}
}
impl From<&ServerEvent<Signed>> for Message { impl From<&ServerEvent<Signed>> for Message {
fn from(value: &ServerEvent<Signed>) -> Self { fn from(value: &ServerEvent<Signed>) -> Self {
Message::text(serde_json::to_string(value).expect("This can't fail")) Message::text(serde_json::to_string(value).expect("This can't fail"))

View file

@ -0,0 +1,138 @@
// 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>.
//! 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<ServerEvent<Unsigned>> {
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() {
awiteb marked this conversation as resolved
Review

Maybe would be better to move this to WsError easier to manage errors
there are also others below this

Maybe would be better to move this to `WsError` easier to manage errors there are also others below this
Review

Right, this should be an error, not a message. I don't know how is this happened

Right, this should be an error, not a message. I don't know how is this happened
Review

I'll remove Message event it is actually useless. I don't know why I thought it was good.

I'll remove `Message` event it is actually useless. I don't know why I thought it was good.
Review

I don't know why I thought it was good.

Well, I remembered when I added it, it was a pain to add a new error (before ws_error macro) so I thought it was good for simple messages.

> I don't know why I thought it was good. Well, I remembered when I added it, it was a pain to add a new error (before `ws_error` macro) so I thought it was good for simple messages.
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<ServerEvent<Unsigned>> {
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
}

View file

@ -0,0 +1,21 @@
// 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>.
//! Websocket event handlers.
mod chat_request;
pub use chat_request::*;

View file

@ -21,6 +21,7 @@ use errors::{WsError, WsResult};
use futures::{channel::mpsc, FutureExt, StreamExt, TryStreamExt}; use futures::{channel::mpsc, FutureExt, StreamExt, TryStreamExt};
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use oxidetalis_core::{cipher::K256Secret, types::PublicKey}; use oxidetalis_core::{cipher::K256Secret, types::PublicKey};
use oxidetalis_entities::prelude::*;
use salvo::{ use salvo::{
handler, handler,
http::StatusError, http::StatusError,
@ -30,15 +31,18 @@ use salvo::{
Response, Response,
Router, Router,
}; };
use sea_orm::DatabaseConnection;
use tokio::{sync::RwLock, task::spawn as tokio_spawn, time::sleep as tokio_sleep}; use tokio::{sync::RwLock, task::spawn as tokio_spawn, time::sleep as tokio_sleep};
mod errors; pub mod errors;
mod events; mod events;
mod handlers;
pub use events::*; pub use events::*;
use uuid::Uuid; use uuid::Uuid;
use crate::{ use crate::{
database::UserTableExt,
extensions::{DepotExt, OnlineUsersExt}, extensions::{DepotExt, OnlineUsersExt},
middlewares, middlewares,
nonce::NonceCache, nonce::NonceCache,
@ -92,6 +96,7 @@ pub async fn user_connected(
depot: &Depot, depot: &Depot,
) -> Result<(), StatusError> { ) -> Result<(), StatusError> {
let nonce_cache = depot.nonce_cache(); let nonce_cache = depot.nonce_cache();
let db_conn = depot.db_conn();
let public_key = let public_key =
utils::extract_public_key(req).expect("The public key was checked in the middleware"); utils::extract_public_key(req).expect("The public key was checked in the middleware");
// FIXME: The config should hold `K256Secret` not `PrivateKey` // FIXME: The config should hold `K256Secret` not `PrivateKey`
@ -100,7 +105,7 @@ pub async fn user_connected(
WebSocketUpgrade::new() WebSocketUpgrade::new()
.upgrade(req, res, move |ws| { .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 .await
} }
@ -108,6 +113,7 @@ pub async fn user_connected(
/// Handle the websocket connection /// Handle the websocket connection
async fn handle_socket( async fn handle_socket(
ws: WebSocket, ws: WebSocket,
db_conn: Arc<DatabaseConnection>,
nonce_cache: Arc<NonceCache>, nonce_cache: Arc<NonceCache>,
user_public_key: PublicKey, user_public_key: PublicKey,
user_shared_secret: [u8; 32], user_shared_secret: [u8; 32],
@ -123,27 +129,47 @@ async fn handle_socket(
}); });
tokio_spawn(fut); tokio_spawn(fut);
let conn_id = Uuid::new_v4(); let conn_id = Uuid::new_v4();
let user = SocketUserData::new(user_public_key, user_shared_secret, sender.clone()); let Ok(user) = db_conn.get_user_by_pubk(&user_public_key).await else {
ONLINE_USERS.add_user(&conn_id, user).await; 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})"); 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 { let fut = async move {
while let Some(Ok(msg)) = user_ws_receiver.next().await { while let Some(Ok(msg)) = user_ws_receiver.next().await {
match handle_ws_msg(msg, &nonce_cache, &user_shared_secret).await { match handle_ws_msg(msg, &nonce_cache, &user_shared_secret).await {
Ok(event) => { Ok(event) => {
if let Some(server_event) = handle_events(event, &conn_id).await { if let Some(server_event) =
if let Err(err) = sender.unbounded_send(Ok(Message::from( handle_events(event, &db_conn, &conn_id, user.as_ref()).await
&server_event.sign(&user_shared_secret), {
))) { if let Err(err) = sender.unbounded_send(Ok(server_event
.sign(&user_shared_secret)
.as_ref()
.into()))
{
log::error!("Websocket Error: {err}"); log::error!("Websocket Error: {err}");
break; break;
} }
}; };
} }
Err(err) => { Err(err) => {
if let Err(err) = sender.unbounded_send(Ok(Message::from( if let Err(err) = sender.unbounded_send(Ok(ServerEvent::from(err)
&ServerEvent::from(err).sign(&user_shared_secret), .sign(&user_shared_secret)
))) { .as_ref()
.into()))
{
log::error!("Websocket Error: {err}"); log::error!("Websocket Error: {err}");
break; break;
}; };
@ -178,13 +204,22 @@ async fn handle_ws_msg(
} }
/// Handle user events, and return the server event if needed /// Handle user events, and return the server event if needed
async fn handle_events(event: ClientEvent, conn_id: &Uuid) -> Option<ServerEvent<Unsigned>> { async fn handle_events(
event: ClientEvent,
db: &DatabaseConnection,
conn_id: &Uuid,
user: Option<&UserModel>,
) -> Option<ServerEvent<Unsigned>> {
match &event.event { match &event.event {
ClientEventType::Ping { .. } => Some(ServerEvent::pong()), ClientEventType::Ping { .. } => Some(ServerEvent::pong()),
ClientEventType::Pong { .. } => { ClientEventType::Pong { .. } => {
ONLINE_USERS.update_pong(conn_id).await; ONLINE_USERS.update_pong(conn_id).await;
None 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
}
} }
} }

View file

@ -12,6 +12,7 @@ rust-version.workspace = true
[dependencies] [dependencies]
sea-orm = { workspace = true } sea-orm = { workspace = true }
chrono = { workspace = true }
[lints.rust] [lints.rust]
unsafe_code = "deny" unsafe_code = "deny"

View file

@ -0,0 +1,57 @@
// OxideTalis Messaging Protocol homeserver core implementation
// Copyright (c) 2024 OxideTalis Developers <otmp@4rs.nl>
//
// 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<Utc>,
}
#[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<UserEntity> for Entity {
fn to() -> RelationDef {
Relation::RecipientId.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View file

@ -19,5 +19,8 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE. // SOFTWARE.
pub mod incoming_chat_requests;
pub mod outgoing_chat_requests;
pub mod prelude; pub mod prelude;
pub mod users; pub mod users;
pub mod users_status;

View file

@ -0,0 +1,57 @@
// OxideTalis Messaging Protocol homeserver core implementation
// Copyright (c) 2024 OxideTalis Developers <otmp@4rs.nl>
//
// 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<Utc>,
}
#[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<UserEntity> for Entity {
fn to() -> RelationDef {
Relation::SenderId.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View file

@ -22,8 +22,10 @@
pub use sea_orm::{ pub use sea_orm::{
ActiveModelTrait, ActiveModelTrait,
ColumnTrait, ColumnTrait,
EntityOrSelect,
EntityTrait, EntityTrait,
IntoActiveModel, IntoActiveModel,
ModelTrait,
Order, Order,
PaginatorTrait, PaginatorTrait,
QueryFilter, QueryFilter,
@ -33,9 +35,31 @@ pub use sea_orm::{
SqlErr, 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::{ pub use super::users::{
ActiveModel as UserActiveModel, ActiveModel as UserActiveModel,
Column as UserColumn, Column as UserColumn,
Entity as UserEntity, Entity as UserEntity,
Model as UserModel, Model as UserModel,
}; };
pub use super::users_status::{
AccessStatus,
ActiveModel as UsersStatusActiveModel,
Column as UsersStatusColumn,
Entity as UsersStatusEntity,
Model as UsersStatusModel,
};

View file

@ -21,16 +21,43 @@
use sea_orm::entity::prelude::*; use sea_orm::entity::prelude::*;
use crate::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "users")] #[sea_orm(table_name = "users")]
pub struct Model { pub struct Model {
#[sea_orm(primary_key)] #[sea_orm(primary_key)]
pub id: i32, pub id: UserId,
pub public_key: String, pub public_key: String,
pub is_admin: bool, pub is_admin: bool,
} }
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] #[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<InChatRequestsEntity> for Entity {
fn to() -> RelationDef {
Relation::InChatRequests.def()
}
}
impl Related<OutChatRequestsEntity> for Entity {
fn to() -> RelationDef {
Relation::OutChatRequests.def()
}
}
impl Related<UsersStatusEntity> for Entity {
fn to() -> RelationDef {
Relation::UsersStatus.def()
}
}
impl ActiveModelBehavior for ActiveModel {} impl ActiveModelBehavior for ActiveModel {}

View file

@ -0,0 +1,66 @@
// OxideTalis Messaging Protocol homeserver core implementation
// Copyright (c) 2024 OxideTalis Developers <otmp@4rs.nl>
//
// 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<Utc>,
}
#[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<UserEntity> for Entity {
fn to() -> RelationDef {
Relation::UserId.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View file

@ -0,0 +1,89 @@
// OxideTalis Messaging Protocol homeserver core implementation
// Copyright (c) 2024 OxideTalis Developers <otmp@4rs.nl>
//
// 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,
}

View file

@ -0,0 +1,94 @@
// OxideTalis Messaging Protocol homeserver core implementation
// Copyright (c) 2024 OxideTalis Developers <otmp@4rs.nl>
//
// 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,
}

View file

@ -0,0 +1,128 @@
// OxideTalis Messaging Protocol homeserver core implementation
// Copyright (c) 2024 OxideTalis Developers <otmp@4rs.nl>
//
// 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")
}
}

View file

@ -34,7 +34,7 @@ impl MigrationTrait for Migration {
.if_not_exists() .if_not_exists()
.col( .col(
ColumnDef::new(Users::Id) ColumnDef::new(Users::Id)
.integer() .big_integer()
.not_null() .not_null()
.auto_increment() .auto_increment()
.primary_key(), .primary_key(),
@ -58,7 +58,7 @@ impl MigrationTrait for Migration {
} }
#[derive(DeriveIden)] #[derive(DeriveIden)]
enum Users { pub enum Users {
Table, Table,
Id, Id,
PublicKey, PublicKey,

View file

@ -19,8 +19,12 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE. // 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; mod create_users_table;
pub struct Migrator; pub struct Migrator;
@ -28,6 +32,11 @@ pub struct Migrator;
#[async_trait::async_trait] #[async_trait::async_trait]
impl MigratorTrait for Migrator { impl MigratorTrait for Migrator {
fn migrations() -> Vec<Box<dyn MigrationTrait>> { fn migrations() -> Vec<Box<dyn MigrationTrait>> {
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),
]
} }
} }