821537186e
- 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
222 lines
7.3 KiB
Rust
222 lines
7.3 KiB
Rust
//! 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(())
|
|
}
|
|
}
|