//! Moderation action history: append-only audit trail of all moderation events. //! //! Some functions (get_history, resolve_action) are not yet wired into routes. use chrono::{DateTime, Utc}; use sqlx::PgPool; use super::{ModerationActionId, ModerationActionType, UserId}; use crate::error::Result; /// A moderation action record. #[derive(Debug, sqlx::FromRow)] #[allow(dead_code)] pub struct DbModerationAction { pub id: ModerationActionId, pub user_id: UserId, pub admin_id: UserId, pub action_type: ModerationActionType, pub reason: String, pub content_ref: Option, pub resolved_at: Option>, pub created_at: DateTime, } /// Record a new moderation action. #[tracing::instrument(skip_all)] pub async fn create_action( pool: &PgPool, user_id: UserId, admin_id: UserId, action_type: ModerationActionType, reason: &str, content_ref: Option<&str>, ) -> Result { let id = sqlx::query_scalar::<_, ModerationActionId>( r#" INSERT INTO moderation_actions (user_id, admin_id, action_type, reason, content_ref) VALUES ($1, $2, $3, $4, $5) RETURNING id "#, ) .bind(user_id) .bind(admin_id) .bind(action_type) .bind(reason) .bind(content_ref) .fetch_one(pool) .await?; Ok(id) } /// Get active (unresolved) moderation actions for a user. #[allow(dead_code)] #[tracing::instrument(skip_all)] pub async fn get_active_actions( pool: &PgPool, user_id: UserId, ) -> Result> { let actions = sqlx::query_as::<_, DbModerationAction>( r#" SELECT * FROM moderation_actions WHERE user_id = $1 AND resolved_at IS NULL ORDER BY created_at DESC "#, ) .bind(user_id) .fetch_all(pool) .await?; Ok(actions) } /// Get full moderation history for a user (active + resolved). #[allow(dead_code)] #[tracing::instrument(skip_all)] pub async fn get_history( pool: &PgPool, user_id: UserId, ) -> Result> { let actions = sqlx::query_as::<_, DbModerationAction>( r#" SELECT * FROM moderation_actions WHERE user_id = $1 ORDER BY created_at DESC LIMIT 100 "#, ) .bind(user_id) .fetch_all(pool) .await?; Ok(actions) } /// Resolve a moderation action (e.g., warning acknowledged, suspension lifted). #[allow(dead_code)] #[tracing::instrument(skip_all)] pub async fn resolve_action(pool: &PgPool, action_id: ModerationActionId) -> Result<()> { sqlx::query( "UPDATE moderation_actions SET resolved_at = NOW() WHERE id = $1", ) .bind(action_id) .execute(pool) .await?; Ok(()) } /// Resolve all active actions of a given type for a user. /// Used when unsuspending (resolves the suspension action). #[tracing::instrument(skip_all)] pub async fn resolve_actions_by_type( pool: &PgPool, user_id: UserId, action_type: ModerationActionType, ) -> Result { let result = sqlx::query( r#" UPDATE moderation_actions SET resolved_at = NOW() WHERE user_id = $1 AND action_type = $2 AND resolved_at IS NULL "#, ) .bind(user_id) .bind(action_type) .execute(pool) .await?; Ok(result.rows_affected()) } /// Resolve a content_removal action by content reference (item ID). #[tracing::instrument(skip_all)] pub async fn resolve_content_removal( pool: &PgPool, content_ref: &str, ) -> Result { let result = sqlx::query( r#" UPDATE moderation_actions SET resolved_at = NOW() WHERE action_type = 'content_removal' AND content_ref = $1 AND resolved_at IS NULL "#, ) .bind(content_ref) .execute(pool) .await?; Ok(result.rows_affected()) }