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:
zhenyi
2026-06-11 12:11:05 +08:00
parent 06e8ee96a5
commit 821537186e
111 changed files with 10458 additions and 385 deletions
+221
View File
@@ -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(())
}
}