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:
parent
221a47cfd0
commit
8888bf2d60
34 changed files with 1860 additions and 122 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -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",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
61
crates/oxidetalis/src/database/in_chat_requests.rs
Normal file
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()
|
||||||
|
})
|
||||||
|
.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.
|
//! 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::*;
|
||||||
|
|
102
crates/oxidetalis/src/database/out_chat_requests.rs
Normal file
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,
|
||||||
|
) -> 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 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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
306
crates/oxidetalis/src/database/user_status.rs
Normal file
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> {
|
||||||
|
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
|
// 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()));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
93
crates/oxidetalis/src/macros.rs
Normal file
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 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?;
|
||||||
|
|
21
crates/oxidetalis/src/parameters/mod.rs
Normal file
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
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};
|
||||||
|
|
||||||
|
#[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
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::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();
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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"))
|
||||||
|
|
138
crates/oxidetalis/src/websocket/handlers/chat_request.rs
Normal file
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() {
|
||||||
|
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
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 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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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"
|
||||||
|
|
57
crates/oxidetalis_entities/src/incoming_chat_requests.rs
Normal file
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
|
// 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;
|
||||||
|
|
57
crates/oxidetalis_entities/src/outgoing_chat_requests.rs
Normal file
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::{
|
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,
|
||||||
|
};
|
||||||
|
|
|
@ -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 {}
|
||||||
|
|
66
crates/oxidetalis_entities/src/users_status.rs
Normal file
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
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()
|
.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,
|
||||||
|
|
|
@ -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),
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue