feat: Chat request implementation

In this patch, I have implemented the chat request and response from OTMP
protocol. The changes include:
- New `in_chat_request` and `out_chat_request` database tables
- New `users_status` table to store user status (whitelisted or blacklisted)
- New server events:
    - `ChatRequest` To send chat request comes from some user
    - `ChatRequestResponse` To send chat request response to some user
- New client events:
    - `ChatRequest` To send chat request to some user
    - `ChatRequestResponse` To send chat request response to some user
- `ws_errors` macro to create websocket errors
- `/user/whitelist` and `/user/blacklist` API to list whitelisted and
blacklisted users

Fixes: #2
Reviewed-on: #14
Reviewed-by: Amjad Alsharafi <me@amjad.alsharafi.dev>
Helped-by: Amjad Alsharafi <me@amjad.alsharafi.dev>
Signed-off-by: Awiteb <a@4rs.nl>
This commit is contained in:
Awiteb 2024-07-18 15:21:05 +03:00
parent 221a47cfd0
commit 8888bf2d60
Signed by: awiteb
GPG key ID: 3F6B55640AA6682F
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()
})
.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,
) -> 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> {
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) {
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};
#[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
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() {
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

@ -11,7 +11,8 @@ 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),
]
} }
} }