feat: Chat request implementation #14
1
Cargo.lock
generated
|
@ -1965,6 +1965,7 @@ dependencies = [
|
|||
name = "oxidetalis_entities"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"sea-orm",
|
||||
]
|
||||
|
||||
|
|
61
crates/oxidetalis/src/database/in_chat_requests.rs
Normal 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
|
||||
.on_conflict(
|
||||
OnConflict::columns([
|
||||
InChatRequestsColumn::RecipientId,
|
||||
InChatRequestsColumn::Sender,
|
||||
])
|
||||
.do_nothing()
|
||||
.to_owned(),
|
||||
)
|
||||
.exec(self)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -16,6 +16,12 @@
|
|||
|
||||
//! Database utilities for the OxideTalis homeserver.
|
||||
|
||||
mod in_chat_requests;
|
||||
mod out_chat_requests;
|
||||
mod user;
|
||||
mod user_status;
|
||||
|
||||
pub use in_chat_requests::*;
|
||||
pub use out_chat_requests::*;
|
||||
pub use user::*;
|
||||
pub use user_status::*;
|
||||
|
|
102
crates/oxidetalis/src/database/out_chat_requests.rs
Normal 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
Amjad50
commented
should return should return `true` based on the name and description of the func
Amjad50
commented
small nitpick (also applies to 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(())
|
||||
}
|
||||
}
|
|
@ -21,27 +21,29 @@ use oxidetalis_core::types::PublicKey;
|
|||
use oxidetalis_entities::prelude::*;
|
||||
use sea_orm::DatabaseConnection;
|
||||
|
||||
use crate::errors::{ApiError, ApiResult};
|
||||
use crate::{errors::ServerResult, routes::ApiError};
|
||||
|
||||
pub trait UserTableExt {
|
||||
/// Returns true if there is users in the database
|
||||
async fn users_exists_in_database(&self) -> ApiResult<bool>;
|
||||
async fn users_exists_in_database(&self) -> ServerResult<bool>;
|
||||
/// Register new user
|
||||
async fn register_user(&self, public_key: &PublicKey, is_admin: bool) -> ApiResult<()>;
|
||||
async fn register_user(&self, public_key: &PublicKey, is_admin: bool) -> ServerResult<()>;
|
||||
/// Returns user by its public key
|
||||
async fn get_user_by_pubk(&self, public_key: &PublicKey) -> ServerResult<Option<UserModel>>;
|
||||
}
|
||||
|
||||
impl UserTableExt for DatabaseConnection {
|
||||
#[logcall]
|
||||
async fn users_exists_in_database(&self) -> ApiResult<bool> {
|
||||
async fn users_exists_in_database(&self) -> ServerResult<bool> {
|
||||
UserEntity::find()
|
||||
.one(self)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
.map(|u| u.is_some())
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
#[logcall]
|
||||
async fn register_user(&self, public_key: &PublicKey, is_admin: bool) -> ApiResult<()> {
|
||||
async fn register_user(&self, public_key: &PublicKey, is_admin: bool) -> ServerResult<()> {
|
||||
if let Err(err) = (UserActiveModel {
|
||||
public_key: Set(public_key.to_string()),
|
||||
is_admin: Set(is_admin),
|
||||
|
@ -51,10 +53,19 @@ impl UserTableExt for DatabaseConnection {
|
|||
.await
|
||||
{
|
||||
if let Some(SqlErr::UniqueConstraintViolation(_)) = err.sql_err() {
|
||||
return Err(ApiError::DuplicatedUser);
|
||||
return Err(ApiError::AlreadyRegistered.into());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[logcall]
|
||||
async fn get_user_by_pubk(&self, public_key: &PublicKey) -> ServerResult<Option<UserModel>> {
|
||||
UserEntity::find()
|
||||
.filter(UserColumn::PublicKey.eq(public_key.to_string()))
|
||||
.one(self)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
|
306
crates/oxidetalis/src/database/user_status.rs
Normal 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
Amjad50
commented
you can use 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)
|
||||
}
|
|
@ -14,24 +14,16 @@
|
|||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://gnu.org/licenses/agpl-3.0>.
|
||||
|
||||
use salvo::{
|
||||
http::StatusCode,
|
||||
oapi::{Components as OapiComponents, EndpointOutRegister, Operation as OapiOperation},
|
||||
Response,
|
||||
Scribe,
|
||||
};
|
||||
use sea_orm::DbErr;
|
||||
|
||||
use crate::{routes::write_json_body, schemas::MessageSchema};
|
||||
use crate::{routes::ApiError, websocket::errors::WsError};
|
||||
|
||||
/// Result type of the homeserver
|
||||
#[allow(clippy::absolute_paths)]
|
||||
pub(crate) type Result<T> = std::result::Result<T, Error>;
|
||||
#[allow(clippy::absolute_paths)]
|
||||
pub type ApiResult<T> = std::result::Result<T, ApiError>;
|
||||
pub(crate) type ServerResult<T> = Result<T, ServerError>;
|
||||
|
||||
/// The homeserver errors
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub(crate) enum Error {
|
||||
pub enum InternalError {
|
||||
#[error("Database Error: {0}")]
|
||||
Database(#[from] sea_orm::DbErr),
|
||||
#[error("{0}")]
|
||||
|
@ -39,43 +31,41 @@ pub(crate) enum Error {
|
|||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ApiError {
|
||||
/// Error from the database (500 Internal Server Error)
|
||||
#[error("Internal server error")]
|
||||
SeaOrm(#[from] sea_orm::DbErr),
|
||||
/// The server registration is closed (403 Forbidden)
|
||||
#[error("Server registration is closed")]
|
||||
RegistrationClosed,
|
||||
/// The entered public key is already registered (400 Bad Request)
|
||||
#[error("The entered public key is already registered")]
|
||||
DuplicatedUser,
|
||||
/// The user entered two different public keys
|
||||
/// one in the header and other in the request body
|
||||
/// (400 Bad Request)
|
||||
#[error("You entered two different public keys")]
|
||||
TwoDifferentKeys,
|
||||
/// The homeserver errors
|
||||
pub enum ServerError {
|
||||
/// Internal server error, should not be exposed to the user
|
||||
#[error("{0}")]
|
||||
Internal(#[from] InternalError),
|
||||
/// API error, errors happening in the API
|
||||
#[error("{0}")]
|
||||
Api(#[from] ApiError),
|
||||
/// WebSocket error, errors happening in the WebSocket
|
||||
#[error("{0}")]
|
||||
Ws(#[from] WsError),
|
||||
}
|
||||
|
||||
impl ApiError {
|
||||
/// Status code of the error
|
||||
pub const fn status_code(&self) -> StatusCode {
|
||||
match self {
|
||||
Self::SeaOrm(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Self::RegistrationClosed => StatusCode::FORBIDDEN,
|
||||
Self::DuplicatedUser | Self::TwoDifferentKeys => StatusCode::BAD_REQUEST,
|
||||
impl From<DbErr> for ServerError {
|
||||
fn from(err: DbErr) -> Self {
|
||||
Self::Internal(err.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ServerError> for WsError {
|
||||
fn from(err: ServerError) -> Self {
|
||||
match err {
|
||||
ServerError::Api(ApiError::NotRegisteredUser) => WsError::RegistredUserEvent,
|
||||
ServerError::Internal(_) | ServerError::Api(_) => WsError::InternalServerError,
|
||||
ServerError::Ws(err) => err,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EndpointOutRegister for ApiError {
|
||||
fn register(_: &mut OapiComponents, _: &mut OapiOperation) {}
|
||||
}
|
||||
|
||||
impl Scribe for ApiError {
|
||||
fn render(self, res: &mut Response) {
|
||||
log::error!("Error: {self}");
|
||||
|
||||
res.status_code(self.status_code());
|
||||
write_json_body(res, MessageSchema::new(self.to_string()));
|
||||
impl From<ServerError> for ApiError {
|
||||
fn from(err: ServerError) -> Self {
|
||||
match err {
|
||||
ServerError::Ws(WsError::RegistredUserEvent) => ApiError::NotRegisteredUser,
|
||||
ServerError::Internal(_) | ServerError::Ws(_) => ApiError::Internal,
|
||||
ServerError::Api(err) => err,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,20 +18,21 @@ use std::sync::Arc;
|
|||
|
||||
use chrono::Utc;
|
||||
use oxidetalis_config::Config;
|
||||
use oxidetalis_core::types::PublicKey;
|
||||
use rayon::iter::{IntoParallelRefMutIterator, ParallelIterator};
|
||||
use salvo::{websocket::Message, Depot};
|
||||
use salvo::Depot;
|
||||
use sea_orm::DatabaseConnection;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
nonce::NonceCache,
|
||||
websocket::{OnlineUsers, ServerEvent, SocketUserData},
|
||||
websocket::{OnlineUsers, ServerEvent, SocketUserData, Unsigned},
|
||||
};
|
||||
|
||||
/// Extension trait for the Depot.
|
||||
pub trait DepotExt {
|
||||
/// Returns the database connection
|
||||
fn db_conn(&self) -> &DatabaseConnection;
|
||||
fn db_conn(&self) -> Arc<DatabaseConnection>;
|
||||
/// Returns the server configuration
|
||||
fn config(&self) -> &Config;
|
||||
/// Retutns the nonce cache
|
||||
|
@ -54,12 +55,20 @@ pub trait OnlineUsersExt {
|
|||
|
||||
/// Disconnect inactive users (who not respond for the ping event)
|
||||
async fn disconnect_inactive_users(&self);
|
||||
|
||||
/// Returns the connection id of the user, if it is online
|
||||
async fn is_online(&self, public_key: &PublicKey) -> Option<Uuid>;
|
||||
|
||||
/// Send an event to user by connection id
|
||||
async fn send(&self, conn_id: &Uuid, event: ServerEvent<Unsigned>);
|
||||
}
|
||||
|
||||
impl DepotExt for Depot {
|
||||
fn db_conn(&self) -> &DatabaseConnection {
|
||||
self.obtain::<Arc<DatabaseConnection>>()
|
||||
.expect("Database connection not found")
|
||||
fn db_conn(&self) -> Arc<DatabaseConnection> {
|
||||
Arc::clone(
|
||||
self.obtain::<Arc<DatabaseConnection>>()
|
||||
.expect("Database connection not found"),
|
||||
)
|
||||
}
|
||||
|
||||
fn config(&self) -> &Config {
|
||||
|
@ -87,9 +96,10 @@ impl OnlineUsersExt for OnlineUsers {
|
|||
let now = Utc::now();
|
||||
self.write().await.par_iter_mut().for_each(|(_, u)| {
|
||||
u.pinged_at = now;
|
||||
let _ = u.sender.unbounded_send(Ok(Message::from(
|
||||
&ServerEvent::ping().sign(&u.shared_secret),
|
||||
)));
|
||||
let _ = u.sender.unbounded_send(Ok(ServerEvent::ping()
|
||||
.sign(&u.shared_secret)
|
||||
.as_ref()
|
||||
.into()));
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -110,4 +120,20 @@ impl OnlineUsersExt for OnlineUsers {
|
|||
true
|
||||
});
|
||||
}
|
||||
|
||||
async fn is_online(&self, public_key: &PublicKey) -> Option<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
Amjad50
commented
use
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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
93
crates/oxidetalis/src/macros.rs
Normal 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
|
||||
),+
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
|
@ -19,6 +19,7 @@
|
|||
|
||||
use std::process::ExitCode;
|
||||
|
||||
use errors::ServerError;
|
||||
use oxidetalis_config::{CliArgs, Parser};
|
||||
use oxidetalis_migrations::MigratorTrait;
|
||||
use salvo::{conn::TcpListener, Listener, Server};
|
||||
|
@ -26,18 +27,21 @@ use salvo::{conn::TcpListener, Listener, Server};
|
|||
mod database;
|
||||
mod errors;
|
||||
mod extensions;
|
||||
mod macros;
|
||||
mod middlewares;
|
||||
mod nonce;
|
||||
mod parameters;
|
||||
mod routes;
|
||||
mod schemas;
|
||||
mod utils;
|
||||
mod websocket;
|
||||
|
||||
async fn try_main() -> errors::Result<()> {
|
||||
async fn try_main() -> errors::ServerResult<()> {
|
||||
pretty_env_logger::init_timed();
|
||||
|
||||
log::info!("Parsing configuration");
|
||||
let config = oxidetalis_config::Config::load(CliArgs::parse())?;
|
||||
let config = oxidetalis_config::Config::load(CliArgs::parse())
|
||||
.map_err(|err| ServerError::Internal(err.into()))?;
|
||||
log::info!("Configuration parsed successfully");
|
||||
log::info!("Connecting to the database");
|
||||
let connection = sea_orm::Database::connect(utils::postgres_url(&config.postgresdb)).await?;
|
||||
|
|
21
crates/oxidetalis/src/parameters/mod.rs
Normal 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::*;
|
131
crates/oxidetalis/src/parameters/pagination.rs
Normal 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
Amjad50
commented
We can add
If we go with this, we can also use 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
awiteb
commented
I'll see it, the reason why I don't put it as an argument, is because As I see, Salvo want it as one parameter, but the pagination 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
awiteb
commented
Ok, I get it. It ask me to implement
So I can just implement the 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),
|
||||
)
|
||||
}
|
77
crates/oxidetalis/src/routes/errors.rs
Normal 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()));
|
||||
}
|
||||
}
|
|
@ -27,8 +27,11 @@ use crate::nonce::NonceCache;
|
|||
use crate::schemas::MessageSchema;
|
||||
use crate::{middlewares, websocket};
|
||||
|
||||
mod errors;
|
||||
mod user;
|
||||
|
||||
pub use errors::*;
|
||||
|
||||
pub fn write_json_body(res: &mut Response, json_body: impl serde::Serialize) {
|
||||
res.write_body(serde_json::to_string(&json_body).expect("Json serialization can't be fail"))
|
||||
.ok();
|
||||
|
|
|
@ -20,18 +20,20 @@ use oxidetalis_core::types::{PublicKey, Signature};
|
|||
use salvo::{
|
||||
http::StatusCode,
|
||||
oapi::{endpoint, extract::JsonBody},
|
||||
writing::Json,
|
||||
Depot,
|
||||
Request,
|
||||
Router,
|
||||
Writer,
|
||||
};
|
||||
|
||||
use super::{ApiError, ApiResult};
|
||||
use crate::{
|
||||
database::UserTableExt,
|
||||
errors::{ApiError, ApiResult},
|
||||
database::{UserTableExt, UsersStatusExt},
|
||||
extensions::DepotExt,
|
||||
middlewares,
|
||||
schemas::{EmptySchema, MessageSchema, RegisterUserBody},
|
||||
parameters::Pagination,
|
||||
schemas::{BlackListedUser, EmptySchema, MessageSchema, RegisterUserBody, WhiteListedUser},
|
||||
utils,
|
||||
};
|
||||
|
||||
|
@ -79,10 +81,90 @@ pub async fn register(
|
|||
Ok(EmptySchema::new(StatusCode::CREATED))
|
||||
}
|
||||
|
||||
/// (🔐) Get whitelisted users
|
||||
#[endpoint(
|
||||
operation_id = "whitelist",
|
||||
tags("User"),
|
||||
responses(
|
||||
(status_code = 200, description = "Returns whitelisted users", content_type = "application/json", body = Vec<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
|
||||
pub fn route() -> Router {
|
||||
Router::new()
|
||||
.push(Router::with_path("register").post(register))
|
||||
.push(Router::with_path("whitelist").get(user_whitelist))
|
||||
.push(Router::with_path("blacklist").get(user_blacklist))
|
||||
.hoop(middlewares::public_key_check)
|
||||
.hoop(middlewares::signature_check)
|
||||
}
|
||||
|
|
|
@ -14,7 +14,11 @@
|
|||
// 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 std::str::FromStr;
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use oxidetalis_core::{cipher::K256Secret, types::PublicKey};
|
||||
use oxidetalis_entities::prelude::*;
|
||||
use salvo::oapi::ToSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
|
@ -25,3 +29,57 @@ pub struct RegisterUserBody {
|
|||
/// The public key of the user
|
||||
pub public_key: PublicKey,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, ToSchema, derive_new::new)]
|
||||
#[salvo(schema(name = WhiteListedUser, example = json!(WhiteListedUser::default())))]
|
||||
pub struct WhiteListedUser {
|
||||
/// User's public key
|
||||
pub public_key: PublicKey,
|
||||
/// When the user was whitelisted
|
||||
pub whitelisted_at: DateTime<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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,42 +16,27 @@
|
|||
|
||||
//! Websocket errors
|
||||
|
||||
use crate::ws_errors;
|
||||
|
||||
/// Result type of websocket
|
||||
pub type WsResult<T> = Result<T, WsError>;
|
||||
|
||||
/// Websocket errors, returned in the websocket communication
|
||||
#[derive(Debug)]
|
||||
pub enum WsError {
|
||||
/// The signature is invalid
|
||||
InvalidSignature,
|
||||
/// Message type must be text
|
||||
NotTextMessage,
|
||||
/// Invalid json data
|
||||
InvalidJsonData,
|
||||
/// Unknown client event
|
||||
UnknownClientEvent,
|
||||
}
|
||||
|
||||
impl WsError {
|
||||
/// Returns error name
|
||||
pub const fn name(&self) -> &'static str {
|
||||
match self {
|
||||
WsError::InvalidSignature => "InvalidSignature",
|
||||
WsError::NotTextMessage => "NotTextMessage",
|
||||
WsError::InvalidJsonData => "InvalidJsonData",
|
||||
WsError::UnknownClientEvent => "UnknownClientEvent",
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the error reason
|
||||
pub const fn reason(&self) -> &'static str {
|
||||
match self {
|
||||
WsError::InvalidSignature => "Invalid event signature",
|
||||
WsError::NotTextMessage => "The websocket message must be text message",
|
||||
WsError::InvalidJsonData => "Received invalid json data, the text must be valid json",
|
||||
WsError::UnknownClientEvent => {
|
||||
"Unknown client event, the event is not recognized by the server"
|
||||
}
|
||||
}
|
||||
}
|
||||
ws_errors! {
|
||||
InternalServerError = "Internal server error",
|
||||
InvalidSignature = "Invalid event signature",
|
||||
NotTextMessage = "The websocket message must be text message",
|
||||
InvalidJsonData = "Received invalid json data, the text must be valid json",
|
||||
UnknownClientEvent = "Unknown client event, the event is not recognized by the server",
|
||||
RegistredUserEvent = "The event is only for registred users",
|
||||
UserNotFound = "The user is not registered in the server",
|
||||
AlreadyOnTheWhitelist = "The user is already on your whitelist",
|
||||
CannotAddSelfToWhitelist = "You cannot add yourself to the whitelist",
|
||||
AlreadyOnTheBlacklist = "The user is already on your blacklist",
|
||||
CannotAddSelfToBlacklist = "You cannot add yourself to the blacklist",
|
||||
AlreadySendChatRequest = "You have already sent a chat request to this user",
|
||||
CannotSendChatRequestToSelf = "You cannot send a chat request to yourself",
|
||||
CannotRespondToOwnChatRequest = "You cannot respond to your own chat request",
|
||||
NoChatRequestFromRecipient = "You do not have a chat request from the recipient",
|
||||
RecipientBlacklist = "You cannot send a chat request because you are on the recipient's blacklist.",
|
||||
AlreadyInRecipientWhitelist = "You are already on the recipient's whitelist and can chat with them."
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
|
||||
//! Events that the client send it
|
||||
|
||||
use oxidetalis_core::types::Signature;
|
||||
use oxidetalis_core::types::{PublicKey, Signature};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{nonce::NonceCache, utils};
|
||||
|
@ -24,10 +24,14 @@ use crate::{nonce::NonceCache, utils};
|
|||
/// Client websocket event
|
||||
#[derive(Deserialize, Clone, Debug)]
|
||||
pub struct ClientEvent {
|
||||
#[serde(flatten)]
|
||||
pub event: ClientEventType,
|
||||
signature: Signature,
|
||||
}
|
||||
|
||||
// ## Important for contuributors
|
||||
// Please make sure to order the event data alphabetically.
|
||||
|
||||
/// Client websocket event type
|
||||
#[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Debug)]
|
||||
#[serde(rename_all = "PascalCase", tag = "event", content = "data")]
|
||||
|
@ -36,6 +40,10 @@ pub enum ClientEventType {
|
|||
Ping { timestamp: u64 },
|
||||
/// Pong event
|
||||
Pong { timestamp: u64 },
|
||||
/// Request to chat with a user
|
||||
ChatRequest { to: PublicKey },
|
||||
/// Response to a chat request
|
||||
ChatRequestResponse { accepted: bool, to: PublicKey },
|
||||
}
|
||||
|
||||
impl ClientEventType {
|
||||
|
|
|
@ -19,7 +19,10 @@
|
|||
use std::marker::PhantomData;
|
||||
|
||||
use chrono::Utc;
|
||||
use oxidetalis_core::{cipher::K256Secret, types::Signature};
|
||||
use oxidetalis_core::{
|
||||
cipher::K256Secret,
|
||||
types::{PublicKey, Signature},
|
||||
};
|
||||
use salvo::websocket::Message;
|
||||
use serde::Serialize;
|
||||
|
||||
|
@ -28,6 +31,7 @@ use crate::websocket::errors::WsError;
|
|||
/// Signed marker, used to indicate that the event is signed
|
||||
pub struct Signed;
|
||||
/// Unsigned marker, used to indicate that the event is unsigned
|
||||
#[derive(Debug)]
|
||||
pub struct Unsigned;
|
||||
|
||||
/// Server websocket event
|
||||
|
@ -42,12 +46,16 @@ pub struct ServerEvent<T> {
|
|||
|
||||
/// server websocket event type
|
||||
#[derive(Serialize, Clone, Eq, PartialEq, Debug)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
#[serde(rename_all = "PascalCase", tag = "event", content = "data")]
|
||||
pub enum ServerEventType {
|
||||
/// Ping event
|
||||
Ping { timestamp: u64 },
|
||||
/// Pong event
|
||||
Pong { timestamp: u64 },
|
||||
/// New chat request from someone
|
||||
ChatRequest { from: PublicKey },
|
||||
/// New chat request response from someone
|
||||
ChatRequestResponse { accepted: bool, from: PublicKey },
|
||||
/// Error event
|
||||
awiteb marked this conversation as resolved
Amjad50
commented
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?
awiteb
commented
Yes, I forgot it Yes, I forgot it
|
||||
Error {
|
||||
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
|
||||
pub fn sign(self, shared_secret: &[u8; 32]) -> 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 {
|
||||
fn from(value: &ServerEvent<Signed>) -> Self {
|
||||
Message::text(serde_json::to_string(value).expect("This can't fail"))
|
||||
|
|
138
crates/oxidetalis/src/websocket/handlers/chat_request.rs
Normal 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
Amjad50
commented
Maybe would be better to move this to Maybe would be better to move this to `WsError` easier to manage errors
there are also others below this
awiteb
commented
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
awiteb
commented
I'll remove I'll remove `Message` event it is actually useless. I don't know why I thought it was good.
awiteb
commented
Well, I remembered when I added it, it was a pain to add a new error (before > 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
|
||||
}
|
21
crates/oxidetalis/src/websocket/handlers/mod.rs
Normal 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::*;
|
|
@ -21,6 +21,7 @@ use errors::{WsError, WsResult};
|
|||
use futures::{channel::mpsc, FutureExt, StreamExt, TryStreamExt};
|
||||
use once_cell::sync::Lazy;
|
||||
use oxidetalis_core::{cipher::K256Secret, types::PublicKey};
|
||||
use oxidetalis_entities::prelude::*;
|
||||
use salvo::{
|
||||
handler,
|
||||
http::StatusError,
|
||||
|
@ -30,15 +31,18 @@ use salvo::{
|
|||
Response,
|
||||
Router,
|
||||
};
|
||||
use sea_orm::DatabaseConnection;
|
||||
use tokio::{sync::RwLock, task::spawn as tokio_spawn, time::sleep as tokio_sleep};
|
||||
|
||||
mod errors;
|
||||
pub mod errors;
|
||||
mod events;
|
||||
mod handlers;
|
||||
|
||||
pub use events::*;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
database::UserTableExt,
|
||||
extensions::{DepotExt, OnlineUsersExt},
|
||||
middlewares,
|
||||
nonce::NonceCache,
|
||||
|
@ -92,6 +96,7 @@ pub async fn user_connected(
|
|||
depot: &Depot,
|
||||
) -> Result<(), StatusError> {
|
||||
let nonce_cache = depot.nonce_cache();
|
||||
let db_conn = depot.db_conn();
|
||||
let public_key =
|
||||
utils::extract_public_key(req).expect("The public key was checked in the middleware");
|
||||
// FIXME: The config should hold `K256Secret` not `PrivateKey`
|
||||
|
@ -100,7 +105,7 @@ pub async fn user_connected(
|
|||
|
||||
WebSocketUpgrade::new()
|
||||
.upgrade(req, res, move |ws| {
|
||||
handle_socket(ws, nonce_cache, public_key, shared_secret)
|
||||
handle_socket(ws, db_conn, nonce_cache, public_key, shared_secret)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
@ -108,6 +113,7 @@ pub async fn user_connected(
|
|||
/// Handle the websocket connection
|
||||
async fn handle_socket(
|
||||
ws: WebSocket,
|
||||
db_conn: Arc<DatabaseConnection>,
|
||||
nonce_cache: Arc<NonceCache>,
|
||||
user_public_key: PublicKey,
|
||||
user_shared_secret: [u8; 32],
|
||||
|
@ -123,27 +129,47 @@ async fn handle_socket(
|
|||
});
|
||||
tokio_spawn(fut);
|
||||
let conn_id = Uuid::new_v4();
|
||||
let user = SocketUserData::new(user_public_key, user_shared_secret, sender.clone());
|
||||
ONLINE_USERS.add_user(&conn_id, user).await;
|
||||
let Ok(user) = db_conn.get_user_by_pubk(&user_public_key).await else {
|
||||
let _ = sender.unbounded_send(Ok(ServerEvent::from(WsError::InternalServerError)
|
||||
.sign(&user_shared_secret)
|
||||
.as_ref()
|
||||
.into()));
|
||||
return;
|
||||
};
|
||||
ONLINE_USERS
|
||||
.add_user(
|
||||
&conn_id,
|
||||
SocketUserData::new(user_public_key, user_shared_secret, sender.clone()),
|
||||
)
|
||||
.await;
|
||||
log::info!("New user connected: ConnId(={conn_id}) PublicKey(={user_public_key})");
|
||||
|
||||
// TODO: Send the incoming chat request to the user, while they are offline.
|
||||
// This after adding last_login col to the user table
|
||||
|
||||
let fut = async move {
|
||||
while let Some(Ok(msg)) = user_ws_receiver.next().await {
|
||||
match handle_ws_msg(msg, &nonce_cache, &user_shared_secret).await {
|
||||
Ok(event) => {
|
||||
if let Some(server_event) = handle_events(event, &conn_id).await {
|
||||
if let Err(err) = sender.unbounded_send(Ok(Message::from(
|
||||
&server_event.sign(&user_shared_secret),
|
||||
))) {
|
||||
if let Some(server_event) =
|
||||
handle_events(event, &db_conn, &conn_id, user.as_ref()).await
|
||||
{
|
||||
if let Err(err) = sender.unbounded_send(Ok(server_event
|
||||
.sign(&user_shared_secret)
|
||||
.as_ref()
|
||||
.into()))
|
||||
{
|
||||
log::error!("Websocket Error: {err}");
|
||||
break;
|
||||
}
|
||||
};
|
||||
}
|
||||
Err(err) => {
|
||||
if let Err(err) = sender.unbounded_send(Ok(Message::from(
|
||||
&ServerEvent::from(err).sign(&user_shared_secret),
|
||||
))) {
|
||||
if let Err(err) = sender.unbounded_send(Ok(ServerEvent::from(err)
|
||||
.sign(&user_shared_secret)
|
||||
.as_ref()
|
||||
.into()))
|
||||
{
|
||||
log::error!("Websocket Error: {err}");
|
||||
break;
|
||||
};
|
||||
|
@ -178,13 +204,22 @@ async fn handle_ws_msg(
|
|||
}
|
||||
|
||||
/// Handle user events, and return the server event if needed
|
||||
async fn handle_events(event: ClientEvent, conn_id: &Uuid) -> Option<ServerEvent<Unsigned>> {
|
||||
async fn handle_events(
|
||||
event: ClientEvent,
|
||||
db: &DatabaseConnection,
|
||||
conn_id: &Uuid,
|
||||
user: Option<&UserModel>,
|
||||
) -> Option<ServerEvent<Unsigned>> {
|
||||
match &event.event {
|
||||
ClientEventType::Ping { .. } => Some(ServerEvent::pong()),
|
||||
ClientEventType::Pong { .. } => {
|
||||
ONLINE_USERS.update_pong(conn_id).await;
|
||||
None
|
||||
}
|
||||
ClientEventType::ChatRequest { to } => handlers::handle_chat_request(db, user, to).await,
|
||||
ClientEventType::ChatRequestResponse { to, accepted } => {
|
||||
handlers::handle_chat_response(db, user, to, *accepted).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -11,7 +11,8 @@ rust-version.workspace = true
|
|||
|
||||
|
||||
[dependencies]
|
||||
sea-orm = {workspace = true }
|
||||
sea-orm = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
|
||||
[lints.rust]
|
||||
unsafe_code = "deny"
|
||||
|
|
57
crates/oxidetalis_entities/src/incoming_chat_requests.rs
Normal 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 {}
|
|
@ -19,5 +19,8 @@
|
|||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
pub mod incoming_chat_requests;
|
||||
pub mod outgoing_chat_requests;
|
||||
pub mod prelude;
|
||||
pub mod users;
|
||||
pub mod users_status;
|
||||
|
|
57
crates/oxidetalis_entities/src/outgoing_chat_requests.rs
Normal 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 {}
|
|
@ -22,8 +22,10 @@
|
|||
pub use sea_orm::{
|
||||
ActiveModelTrait,
|
||||
ColumnTrait,
|
||||
EntityOrSelect,
|
||||
EntityTrait,
|
||||
IntoActiveModel,
|
||||
ModelTrait,
|
||||
Order,
|
||||
PaginatorTrait,
|
||||
QueryFilter,
|
||||
|
@ -33,9 +35,31 @@ pub use sea_orm::{
|
|||
SqlErr,
|
||||
};
|
||||
|
||||
/// User ID type
|
||||
pub type UserId = i64;
|
||||
|
||||
pub use super::incoming_chat_requests::{
|
||||
ActiveModel as InChatRequestsActiveModel,
|
||||
Column as InChatRequestsColumn,
|
||||
Entity as InChatRequestsEntity,
|
||||
Model as InChatRequestsModel,
|
||||
};
|
||||
pub use super::outgoing_chat_requests::{
|
||||
ActiveModel as OutChatRequestsActiveModel,
|
||||
Column as OutChatRequestsColumn,
|
||||
Entity as OutChatRequestsEntity,
|
||||
Model as OutChatRequestsModel,
|
||||
};
|
||||
pub use super::users::{
|
||||
ActiveModel as UserActiveModel,
|
||||
Column as UserColumn,
|
||||
Entity as UserEntity,
|
||||
Model as UserModel,
|
||||
};
|
||||
pub use super::users_status::{
|
||||
AccessStatus,
|
||||
ActiveModel as UsersStatusActiveModel,
|
||||
Column as UsersStatusColumn,
|
||||
Entity as UsersStatusEntity,
|
||||
Model as UsersStatusModel,
|
||||
};
|
||||
|
|
|
@ -21,16 +21,43 @@
|
|||
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
|
||||
#[sea_orm(table_name = "users")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i32,
|
||||
pub id: UserId,
|
||||
pub public_key: String,
|
||||
pub is_admin: bool,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {}
|
||||
pub enum Relation {
|
||||
#[sea_orm(has_many = "InChatRequestsEntity")]
|
||||
InChatRequests,
|
||||
#[sea_orm(has_many = "OutChatRequestsEntity")]
|
||||
OutChatRequests,
|
||||
#[sea_orm(has_many = "UsersStatusEntity")]
|
||||
UsersStatus,
|
||||
}
|
||||
|
||||
impl Related<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 {}
|
||||
|
|
66
crates/oxidetalis_entities/src/users_status.rs
Normal 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 {}
|
|
@ -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,
|
||||
}
|
|
@ -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,
|
||||
}
|
128
crates/oxidetalis_migrations/src/create_users_status.rs
Normal 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")
|
||||
}
|
||||
}
|
|
@ -34,7 +34,7 @@ impl MigrationTrait for Migration {
|
|||
.if_not_exists()
|
||||
.col(
|
||||
ColumnDef::new(Users::Id)
|
||||
.integer()
|
||||
.big_integer()
|
||||
.not_null()
|
||||
.auto_increment()
|
||||
.primary_key(),
|
||||
|
@ -58,7 +58,7 @@ impl MigrationTrait for Migration {
|
|||
}
|
||||
|
||||
#[derive(DeriveIden)]
|
||||
enum Users {
|
||||
pub enum Users {
|
||||
Table,
|
||||
Id,
|
||||
PublicKey,
|
||||
|
|
|
@ -19,8 +19,12 @@
|
|||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
pub use sea_orm_migration::prelude::*;
|
||||
use sea_orm_migration::prelude::*;
|
||||
pub use sea_orm_migration::MigratorTrait;
|
||||
|
||||
mod create_incoming_chat_requests_table;
|
||||
mod create_outgoing_chat_requests_table;
|
||||
mod create_users_status;
|
||||
mod create_users_table;
|
||||
|
||||
pub struct Migrator;
|
||||
|
@ -28,6 +32,11 @@ pub struct Migrator;
|
|||
#[async_trait::async_trait]
|
||||
impl MigratorTrait for Migrator {
|
||||
fn migrations() -> Vec<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),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
since we are not dealing with the result. maybe a one statement approach to do
here i'm updating
InOn
if needed, but can also be changed todo_nothing()