//! Moderation action history: append-only audit trail of all moderation events. 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(crate) 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(crate) async fn create_action( pool: &PgPool, user_id: UserId, admin_id: crate::auth::AdminId, 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.get()) .bind(action_type) .bind(reason) .bind(content_ref) .fetch_one(pool) .await?; Ok(id) } /// Get active (unresolved) moderation actions for a user. #[tracing::instrument(skip_all)] pub(crate) 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). #[tracing::instrument(skip_all)] pub(crate) 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 all active actions of a given type for a user. /// Used when unsuspending (resolves the suspension action). #[tracing::instrument(skip_all)] pub(crate) 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(crate) 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()) }