refactor(tests): reformat code and update dependency management
- Reorganized import statements in adapter tests for better readability - Replaced or_insert_with(Vec::new) with or_default() in test closures - Updated Cargo.lock with new dependency versions and checksums - Added TLS features to tonic dependency configuration - Included sqlx, chrono, and uuid dependencies with specific features - Added jsonwebtoken and arc-swap as project dependencies - Reformatted assertion statements to comply with line length limits - Adjusted base64 import order in engine codec module - Updated protobuf include statement formatting
This commit is contained in:
+221
@@ -0,0 +1,221 @@
|
||||
//! Forum article event handlers on `MessageService`.
|
||||
//!
|
||||
//! Articles are long-form posts in forum channels. Creating an article
|
||||
//! creates both a `message` (with `message_type = "article"`) and a
|
||||
//! `message_article` row linked to it.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::ImksError;
|
||||
use crate::models::message::MessageType;
|
||||
use crate::models::message_article::ArticleSort;
|
||||
use crate::repo::CreateMessageInput;
|
||||
use crate::socket::socket::Socket;
|
||||
|
||||
use super::message::MessageService;
|
||||
|
||||
impl MessageService {
|
||||
/// Handle `article:create` — create a forum article (message + article metadata).
|
||||
pub async fn create_article(
|
||||
&self,
|
||||
socket: Arc<Socket>,
|
||||
data: &serde_json::Value,
|
||||
) -> crate::ImksResult<()> {
|
||||
let user_id = self.user_id(&socket)?;
|
||||
let arr = data
|
||||
.as_array()
|
||||
.and_then(|a| a.first())
|
||||
.ok_or_else(|| ImksError::InvalidInput("Expected [payload] array".into()))?;
|
||||
|
||||
let channel_id: Uuid = Self::parse_field(arr, "channel_id")?;
|
||||
let title: String = Self::parse_field(arr, "title")?;
|
||||
let body: String = Self::parse_field(arr, "body")?;
|
||||
let summary: Option<String> = Self::parse_optional(arr, "summary")?;
|
||||
let cover_url: Option<String> = Self::parse_optional(arr, "cover_url")?;
|
||||
let tags: Option<serde_json::Value> = Self::parse_optional(arr, "tags")?;
|
||||
|
||||
self.validate_channel_write(&channel_id.to_string(), &user_id.to_string())
|
||||
.await?;
|
||||
|
||||
// Create the message first (with article type)
|
||||
let input = CreateMessageInput {
|
||||
channel_id,
|
||||
author_id: user_id,
|
||||
thread_id: None,
|
||||
reply_to_message_id: None,
|
||||
message_type: MessageType::Article.as_str().into(),
|
||||
body,
|
||||
metadata: None,
|
||||
system: false,
|
||||
};
|
||||
|
||||
let message = self.repo.create(&input).await?;
|
||||
|
||||
// Create the article record
|
||||
let article = self
|
||||
.repo
|
||||
.create_article(
|
||||
message.id,
|
||||
&title,
|
||||
summary.as_deref(),
|
||||
cover_url.as_deref(),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
tags.as_ref(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(ns) = self.namespaces.get_namespace(&socket.namespace) {
|
||||
ns.emit_to_room(
|
||||
&channel_id.to_string(),
|
||||
"article:created",
|
||||
serde_json::json!({
|
||||
"message": message,
|
||||
"article": article,
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
tracing::info!(article_id = %article.id, %channel_id, %user_id, "Article created");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Handle `article:update` — update article title, summary, cover, tags.
|
||||
pub async fn update_article(
|
||||
&self,
|
||||
socket: Arc<Socket>,
|
||||
data: &serde_json::Value,
|
||||
) -> crate::ImksResult<()> {
|
||||
let user_id = self.user_id(&socket)?;
|
||||
let arr = data
|
||||
.as_array()
|
||||
.and_then(|a| a.first())
|
||||
.ok_or_else(|| ImksError::InvalidInput("Expected [payload] array".into()))?;
|
||||
|
||||
let message_id: Uuid = Self::parse_field(arr, "message_id")?;
|
||||
let title: Option<String> = Self::parse_optional(arr, "title")?;
|
||||
let summary: Option<String> = Self::parse_optional(arr, "summary")?;
|
||||
let cover_url: Option<String> = Self::parse_optional(arr, "cover_url")?;
|
||||
let cover_color: Option<String> = Self::parse_optional(arr, "cover_color")?;
|
||||
let tags: Option<serde_json::Value> = Self::parse_optional(arr, "tags")?;
|
||||
|
||||
let existing = self
|
||||
.repo
|
||||
.get(message_id)
|
||||
.await?
|
||||
.ok_or_else(|| ImksError::NotFound(format!("message {message_id}")))?;
|
||||
self.ensure_author_or_mod(
|
||||
existing.author_id,
|
||||
&existing.channel_id.to_string(),
|
||||
user_id,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Update article body if provided
|
||||
if let Ok(new_body) = Self::parse_field::<String>(arr, "body")
|
||||
&& !new_body.is_empty()
|
||||
{
|
||||
let old_body = existing.body.clone();
|
||||
self.repo.update_body(message_id, &new_body).await?;
|
||||
self.repo
|
||||
.record_edit(message_id, user_id, &old_body, &new_body)
|
||||
.await?;
|
||||
}
|
||||
|
||||
if let Some(updated) = self
|
||||
.repo
|
||||
.update_article(
|
||||
message_id,
|
||||
title.as_deref(),
|
||||
summary.as_deref(),
|
||||
cover_url.as_deref(),
|
||||
cover_color.as_deref(),
|
||||
tags.as_ref(),
|
||||
)
|
||||
.await?
|
||||
&& let Some(ns) = self.namespaces.get_namespace(&socket.namespace)
|
||||
{
|
||||
ns.emit_to_room(
|
||||
&existing.channel_id.to_string(),
|
||||
"article:updated",
|
||||
serde_json::to_value(&updated).unwrap_or_default(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Handle `article:list` — list articles in a forum channel.
|
||||
pub async fn list_articles(
|
||||
&self,
|
||||
socket: Arc<Socket>,
|
||||
data: &serde_json::Value,
|
||||
) -> crate::ImksResult<()> {
|
||||
let user_id = self.user_id(&socket)?;
|
||||
let arr = data
|
||||
.as_array()
|
||||
.and_then(|a| a.first())
|
||||
.ok_or_else(|| ImksError::InvalidInput("Expected [payload] array".into()))?;
|
||||
|
||||
let channel_id: Uuid = Self::parse_field(arr, "channel_id")?;
|
||||
self.ensure_readable(&channel_id.to_string(), &user_id.to_string())
|
||||
.await?;
|
||||
self.ensure_member(&channel_id.to_string(), &user_id.to_string())
|
||||
.await?;
|
||||
let before: Option<(i64, Uuid)> = None;
|
||||
let limit: Option<i64> = Self::parse_optional(arr, "limit")?;
|
||||
|
||||
let page = self
|
||||
.repo
|
||||
.list_articles(channel_id, ArticleSort::LatestActivity, before, limit)
|
||||
.await?;
|
||||
let _ = socket.emit(
|
||||
"article:loaded",
|
||||
serde_json::to_value(&page).unwrap_or_default(),
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Handle `article:delete` — soft-delete an article.
|
||||
pub async fn delete_article(
|
||||
&self,
|
||||
socket: Arc<Socket>,
|
||||
data: &serde_json::Value,
|
||||
) -> crate::ImksResult<()> {
|
||||
let user_id = self.user_id(&socket)?;
|
||||
let arr = data
|
||||
.as_array()
|
||||
.and_then(|a| a.first())
|
||||
.ok_or_else(|| ImksError::InvalidInput("Expected [payload] array".into()))?;
|
||||
|
||||
let message_id: Uuid = Self::parse_field(arr, "message_id")?;
|
||||
let _channel_id: Uuid = Self::parse_field(arr, "channel_id")?;
|
||||
|
||||
let existing = self
|
||||
.repo
|
||||
.get(message_id)
|
||||
.await?
|
||||
.ok_or_else(|| ImksError::NotFound(format!("message {message_id}")))?;
|
||||
self.ensure_author_or_mod(
|
||||
existing.author_id,
|
||||
&existing.channel_id.to_string(),
|
||||
user_id,
|
||||
)
|
||||
.await?;
|
||||
|
||||
self.repo.soft_delete(message_id).await?;
|
||||
|
||||
if let Some(ns) = self.namespaces.get_namespace(&socket.namespace) {
|
||||
ns.emit_to_room(&existing.channel_id.to_string(), "article:deleted",
|
||||
serde_json::json!({"id": message_id.to_string(), "channel_id": existing.channel_id.to_string()}),
|
||||
).await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user