This commit is contained in:
2026-08-16 13:00:29 +02:00
parent e6d6968e52
commit 7fc8edd1d0
71 changed files with 2359 additions and 448 deletions
+1 -3
View File
@@ -4,9 +4,7 @@ use crate::models::{channel, role};
use crate::permissions::PermissionSet;
use crate::services::ServicesContext;
use crate::services::permission::PermissionService;
use sea_orm::{
ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set, TransactionTrait,
};
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set, TransactionTrait};
use std::sync::Arc;
use uuid::Uuid;
+150
View File
@@ -0,0 +1,150 @@
use crate::http::error::HTTPError;
use crate::models::emoji;
use crate::services::ServicesContext;
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set, TransactionTrait};
use sha2::{Digest, Sha256};
use std::{
path::{Path, PathBuf},
sync::Arc,
};
use tokio::fs;
use uuid::Uuid;
#[derive(Debug, Clone)]
pub struct EmojiService {
context: Arc<ServicesContext>,
}
impl EmojiService {
pub fn new(context: Arc<ServicesContext>) -> Self {
Self { context }
}
pub fn normalize_alias(alias: &str) -> Result<String, HTTPError> {
let alias = alias.trim().trim_matches(':').to_lowercase();
if alias.is_empty()
|| alias.len() > 64
|| !alias
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '+')
{
return Err(HTTPError::BadRequest("Invalid emoji alias".into()));
}
Ok(alias)
}
pub async fn aliases_available(
&self,
aliases: &[String],
server_id: Option<Uuid>,
except: Option<Uuid>,
) -> Result<(), HTTPError> {
let normalized = aliases
.iter()
.map(|a| Self::normalize_alias(a))
.collect::<Result<Vec<_>, _>>()?;
let mut unique = std::collections::HashSet::new();
if normalized.iter().any(|alias| !unique.insert(alias)) {
return Err(HTTPError::BadRequest("Duplicate emoji alias".into()));
}
let scoped = self
.context
.repositories
.emoji
.list_exact_scope(server_id)
.await?;
for model in scoped {
if Some(model.id) == except {
continue;
}
for alias in self.context.repositories.emoji.aliases(model.id).await? {
if normalized.iter().any(|candidate| candidate == &alias.alias) {
return Err(HTTPError::BadRequest(
"Emoji alias already exists in this scope".into(),
));
}
}
}
Ok(())
}
pub async fn create(
&self,
model: emoji::ActiveModel,
aliases: Vec<String>,
) -> Result<emoji::Model, HTTPError> {
let db = &self.context.repositories.emoji.context.db;
let aliases = aliases
.into_iter()
.map(|a| Self::normalize_alias(&a))
.collect::<Result<Vec<_>, _>>()?;
let server_id = match &model.server_id {
sea_orm::ActiveValue::Set(value) => *value,
_ => None,
};
self.aliases_available(&aliases, server_id, None).await?;
let result = db
.transaction::<_, emoji::Model, anyhow::Error>(|txn| {
Box::pin(async move {
let model = model.insert(txn).await?;
for alias in aliases {
crate::models::emoji_alias::ActiveModel {
emoji_id: Set(model.id),
alias: Set(alias),
..Default::default()
}
.insert(txn)
.await?;
}
Ok(model)
})
})
.await
.map_err(|e| HTTPError::Internal(anyhow::anyhow!(e)))?;
Ok(result)
}
pub async fn replace_aliases(&self, id: Uuid, aliases: Vec<String>) -> Result<(), HTTPError> {
let aliases = aliases
.into_iter()
.map(|a| Self::normalize_alias(&a))
.collect::<Result<Vec<_>, _>>()?;
let db = &self.context.repositories.emoji.context.db;
let txn = db.begin().await?;
crate::models::emoji_alias::Entity::delete_many()
.filter(crate::models::emoji_alias::Column::EmojiId.eq(id))
.exec(&txn)
.await?;
for alias in aliases {
crate::models::emoji_alias::ActiveModel {
emoji_id: Set(id),
alias: Set(alias),
..Default::default()
}
.insert(&txn)
.await?;
}
txn.commit().await?;
Ok(())
}
pub fn hash(data: &[u8]) -> String {
format!("{:x}", Sha256::digest(data))
}
pub async fn save_asset(
root: &Path,
id: Uuid,
data: &[u8],
) -> Result<(String, String), HTTPError> {
let dir = root.join("emoji");
fs::create_dir_all(&dir)
.await
.map_err(|e| HTTPError::InternalServerError(e.to_string()))?;
let relative = PathBuf::from("emoji").join(id.to_string());
let path = root.join(&relative);
fs::write(&path, data)
.await
.map_err(|e| HTTPError::InternalServerError(e.to_string()))?;
Ok((relative.to_string_lossy().into_owned(), Self::hash(data)))
}
pub async fn remove_asset(root: &Path, path: Option<&str>) {
if let Some(path) = path {
let _ = fs::remove_file(root.join(path)).await;
}
}
}
+5
View File
@@ -1,6 +1,7 @@
use crate::repositories::Repositories;
use crate::services::category::CategoryService;
use crate::services::channel::ChannelService;
use crate::services::emoji::EmojiService;
use crate::services::message::MessageService;
use crate::services::permission::PermissionService;
use crate::services::permission_sync::PermissionSyncService;
@@ -13,6 +14,7 @@ use std::sync::{Arc, OnceLock};
pub mod category;
pub mod channel;
pub mod emoji;
pub mod message;
mod permission;
pub mod permission_sync;
@@ -41,6 +43,7 @@ pub struct Services {
pub user: Arc<UserService>,
pub role: Arc<RoleService>,
pub permission: Arc<PermissionService>,
pub emoji: Arc<EmojiService>,
}
impl Services {
@@ -60,6 +63,7 @@ impl Services {
let user = Arc::new(UserService::new(service_context.clone()));
let role = Arc::new(RoleService::new(service_context.clone()));
let permission = Arc::new(PermissionService::new(service_context.clone()));
let emoji = Arc::new(EmojiService::new(service_context.clone()));
let services = Self {
realtime_registry,
@@ -72,6 +76,7 @@ impl Services {
user,
role,
permission,
emoji,
};
let _ = service_context.services.set(services.clone());
services
+80 -24
View File
@@ -27,8 +27,14 @@ impl RealtimeRegistry {
{
continue;
}
channel_users.entry(permission.resource_id).or_default().insert(permission.user_id);
user_channels.entry(permission.user_id).or_default().insert(permission.resource_id);
channel_users
.entry(permission.resource_id)
.or_default()
.insert(permission.user_id);
user_channels
.entry(permission.user_id)
.or_default()
.insert(permission.resource_id);
}
*self.channel_users.write() = channel_users;
@@ -37,20 +43,32 @@ impl RealtimeRegistry {
}
pub fn users_for_channel(&self, channel_id: Uuid) -> HashSet<Uuid> {
self.channel_users.read().get(&channel_id).cloned().unwrap_or_default()
self.channel_users
.read()
.get(&channel_id)
.cloned()
.unwrap_or_default()
}
pub fn set_user_channels(&self, user_id: Uuid, channels: impl IntoIterator<Item = Uuid>) {
let channels: HashSet<_> = channels.into_iter().collect();
let old = self.user_channels.write().insert(user_id, channels.clone()).unwrap_or_default();
let old = self
.user_channels
.write()
.insert(user_id, channels.clone())
.unwrap_or_default();
let mut by_channel = self.channel_users.write();
for channel_id in old.difference(&channels) {
if let Some(users) = by_channel.get_mut(channel_id) {
users.remove(&user_id);
if users.is_empty() { by_channel.remove(channel_id); }
if users.is_empty() {
by_channel.remove(channel_id);
}
}
}
for channel_id in channels { by_channel.entry(channel_id).or_default().insert(user_id); }
for channel_id in channels {
by_channel.entry(channel_id).or_default().insert(user_id);
}
}
pub fn remove_user(&self, user_id: Uuid) {
@@ -59,7 +77,9 @@ impl RealtimeRegistry {
for channel_id in channels {
if let Some(users) = by_channel.get_mut(&channel_id) {
users.remove(&user_id);
if users.is_empty() { by_channel.remove(&channel_id); }
if users.is_empty() {
by_channel.remove(&channel_id);
}
}
}
}
@@ -71,33 +91,69 @@ impl RealtimeRegistry {
for user_id in users {
if let Some(channels) = by_user.get_mut(&user_id) {
channels.remove(&channel_id);
if channels.is_empty() { by_user.remove(&user_id); }
if channels.is_empty() {
by_user.remove(&user_id);
}
}
}
}
}
pub fn start_listening(self: &Arc<Self>, repositories: Arc<Repositories>, event_bus: Arc<EventBus>) {
pub fn start_listening(
self: &Arc<Self>,
repositories: Arc<Repositories>,
event_bus: Arc<EventBus>,
) {
let registry = Arc::clone(self);
event_bus.on_async_with("channel_user_permission_updated", repositories.clone(), move |repositories, (_channel_id, user_id, _permissions): (Uuid, Uuid, u64)| {
let registry = Arc::clone(&registry);
async move {
match repositories.computed_permission.get_all().await {
Ok(all) => registry.set_user_channels(user_id, all.into_iter().filter(|p| p.user_id == user_id && p.scope_type == PermissionScopeType::Channel && ChannelPermission::from_bits_retain(p.permissions as u64).contains(ChannelPermission::READ_CHANNEL)).map(|p| p.resource_id)),
Err(error) => tracing::error!(%user_id, ?error, "Unable to refresh realtime registry"),
event_bus.on_async_with(
"channel_user_permission_updated",
repositories.clone(),
move |repositories, (_channel_id, user_id, _permissions): (Uuid, Uuid, u64)| {
let registry = Arc::clone(&registry);
async move {
match repositories.computed_permission.get_all().await {
Ok(all) => registry.set_user_channels(
user_id,
all.into_iter()
.filter(|p| {
p.user_id == user_id
&& p.scope_type == PermissionScopeType::Channel
&& ChannelPermission::from_bits_retain(p.permissions as u64)
.contains(ChannelPermission::READ_CHANNEL)
})
.map(|p| p.resource_id),
),
Err(error) => {
tracing::error!(%user_id, ?error, "Unable to refresh realtime registry")
}
}
}
}
});
},
);
let registry = Arc::clone(self);
let repositories = repositories.clone();
event_bus.on_async_with("server_user_permission_updated", repositories, move |repositories, (_server_id, user_id): (Uuid, Uuid)| {
let registry = Arc::clone(&registry);
async move {
if let Ok(all) = repositories.computed_permission.get_all().await {
registry.set_user_channels(user_id, all.into_iter().filter(|p| p.user_id == user_id && p.scope_type == PermissionScopeType::Channel && ChannelPermission::from_bits_retain(p.permissions as u64).contains(ChannelPermission::READ_CHANNEL)).map(|p| p.resource_id));
event_bus.on_async_with(
"server_user_permission_updated",
repositories,
move |repositories, (_server_id, user_id): (Uuid, Uuid)| {
let registry = Arc::clone(&registry);
async move {
if let Ok(all) = repositories.computed_permission.get_all().await {
registry.set_user_channels(
user_id,
all.into_iter()
.filter(|p| {
p.user_id == user_id
&& p.scope_type == PermissionScopeType::Channel
&& ChannelPermission::from_bits_retain(p.permissions as u64)
.contains(ChannelPermission::READ_CHANNEL)
})
.map(|p| p.resource_id),
);
}
}
}
});
},
);
}
}
+26 -8
View File
@@ -1,5 +1,5 @@
use crate::services::ServicesContext;
use crate::models::role;
use crate::services::ServicesContext;
use sea_orm::{ActiveModelTrait, EntityTrait, TransactionTrait};
use std::sync::Arc;
use uuid::Uuid;
@@ -56,9 +56,7 @@ impl RoleService {
let txn = db.begin().await?;
let res = role::Entity::delete_by_id(id)
.exec(&txn)
.await?;
let res = role::Entity::delete_by_id(id).exec(&txn).await?;
let deleted = res.rows_affected > 0;
@@ -71,8 +69,18 @@ impl RoleService {
Ok(deleted)
}
pub async fn add_member(&self, role_id: Uuid, user_id: Uuid, server_id: Uuid) -> Result<bool, anyhow::Error> {
let added = self.service_context.repositories.role.add_member(role_id, user_id).await?;
pub async fn add_member(
&self,
role_id: Uuid,
user_id: Uuid,
server_id: Uuid,
) -> Result<bool, anyhow::Error> {
let added = self
.service_context
.repositories
.role
.add_member(role_id, user_id)
.await?;
if added {
self.service_context
.event_bus
@@ -81,8 +89,18 @@ impl RoleService {
Ok(added)
}
pub async fn remove_member(&self, role_id: Uuid, user_id: Uuid, server_id: Uuid) -> Result<bool, anyhow::Error> {
let removed = self.service_context.repositories.role.remove_member(role_id, user_id).await?;
pub async fn remove_member(
&self,
role_id: Uuid,
user_id: Uuid,
server_id: Uuid,
) -> Result<bool, anyhow::Error> {
let removed = self
.service_context
.repositories
.role
.remove_member(role_id, user_id)
.await?;
if removed {
self.service_context
.event_bus
+6 -5
View File
@@ -1,7 +1,10 @@
use crate::models::{role, server};
use crate::repositories::Repositories;
use crate::services::ServicesContext;
use crate::models::{role, server};
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, QuerySelect, QueryOrder, TransactionTrait, Set};
use sea_orm::{
ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, QueryOrder, QuerySelect, Set,
TransactionTrait,
};
use std::sync::Arc;
use uuid::Uuid;
@@ -82,9 +85,7 @@ impl ServerService {
let txn = db.begin().await?;
let res = server::Entity::delete_by_id(id)
.exec(&txn)
.await?;
let res = server::Entity::delete_by_id(id).exec(&txn).await?;
let deleted = res.rows_affected > 0;
+11 -10
View File
@@ -1,7 +1,9 @@
use crate::services::ServicesContext;
use crate::models::{role, user};
use crate::auth::password;
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, IntoActiveModel, QueryFilter, TransactionTrait, Set};
use crate::models::{role, user};
use crate::services::ServicesContext;
use sea_orm::{
ActiveModelTrait, ColumnTrait, EntityTrait, IntoActiveModel, QueryFilter, Set, TransactionTrait,
};
use std::sync::Arc;
use uuid::Uuid;
@@ -65,10 +67,11 @@ impl UserService {
let mut active = user_model.into_active_model();
let password_to_hash = password_str.clone();
let hashed = tokio::task::spawn_blocking(move || password::hash_password(&password_to_hash))
.await
.map_err(|e| anyhow::anyhow!("Join error: {}", e))?
.map_err(|e| anyhow::anyhow!("Password hashing failed: {}", e))?;
let hashed =
tokio::task::spawn_blocking(move || password::hash_password(&password_to_hash))
.await
.map_err(|e| anyhow::anyhow!("Join error: {}", e))?
.map_err(|e| anyhow::anyhow!("Password hashing failed: {}", e))?;
active.password = Set(hashed);
@@ -87,9 +90,7 @@ impl UserService {
let txn = db.begin().await?;
let res = user::Entity::delete_by_id(id)
.exec(&txn)
.await?;
let res = user::Entity::delete_by_id(id).exec(&txn).await?;
let deleted = res.rows_affected > 0;