Skip to main content

max / makenotwork

3.3 KB · 128 lines History Blame Raw
1 //! Moderation action history: append-only audit trail of all moderation events.
2
3 use chrono::{DateTime, Utc};
4 use sqlx::PgPool;
5
6 use super::{ModerationActionId, ModerationActionType, UserId};
7 use crate::error::Result;
8
9 /// A moderation action record.
10 #[derive(Debug, sqlx::FromRow)]
11 #[allow(dead_code)]
12 pub(crate) struct DbModerationAction {
13 pub id: ModerationActionId,
14 pub user_id: UserId,
15 pub admin_id: UserId,
16 pub action_type: ModerationActionType,
17 pub reason: String,
18 pub content_ref: Option<String>,
19 pub resolved_at: Option<DateTime<Utc>>,
20 pub created_at: DateTime<Utc>,
21 }
22
23 /// Record a new moderation action.
24 #[tracing::instrument(skip_all)]
25 pub(crate) async fn create_action(
26 pool: &PgPool,
27 user_id: UserId,
28 admin_id: crate::auth::AdminId,
29 action_type: ModerationActionType,
30 reason: &str,
31 content_ref: Option<&str>,
32 ) -> Result<ModerationActionId> {
33 let id = sqlx::query_scalar::<_, ModerationActionId>(
34 r"
35 INSERT INTO moderation_actions (user_id, admin_id, action_type, reason, content_ref)
36 VALUES ($1, $2, $3, $4, $5)
37 RETURNING id
38 ",
39 )
40 .bind(user_id)
41 .bind(admin_id.get())
42 .bind(action_type)
43 .bind(reason)
44 .bind(content_ref)
45 .fetch_one(pool)
46 .await?;
47
48 Ok(id)
49 }
50
51 /// Get active (unresolved) moderation actions for a user.
52 #[tracing::instrument(skip_all)]
53 pub(crate) async fn get_active_actions(
54 pool: &PgPool,
55 user_id: UserId,
56 ) -> Result<Vec<DbModerationAction>> {
57 let actions = sqlx::query_as::<_, DbModerationAction>(
58 r"
59 SELECT * FROM moderation_actions
60 WHERE user_id = $1 AND resolved_at IS NULL
61 ORDER BY created_at DESC
62 ",
63 )
64 .bind(user_id)
65 .fetch_all(pool)
66 .await?;
67
68 Ok(actions)
69 }
70
71 /// Get full moderation history for a user (active + resolved).
72 #[tracing::instrument(skip_all)]
73 pub(crate) async fn get_history(pool: &PgPool, user_id: UserId) -> Result<Vec<DbModerationAction>> {
74 let actions = sqlx::query_as::<_, DbModerationAction>(
75 r"
76 SELECT * FROM moderation_actions
77 WHERE user_id = $1
78 ORDER BY created_at DESC
79 LIMIT 100
80 ",
81 )
82 .bind(user_id)
83 .fetch_all(pool)
84 .await?;
85
86 Ok(actions)
87 }
88
89 /// Resolve all active actions of a given type for a user.
90 /// Used when unsuspending (resolves the suspension action).
91 #[tracing::instrument(skip_all)]
92 pub(crate) async fn resolve_actions_by_type(
93 pool: &PgPool,
94 user_id: UserId,
95 action_type: ModerationActionType,
96 ) -> Result<u64> {
97 let result = sqlx::query(
98 r"
99 UPDATE moderation_actions
100 SET resolved_at = NOW()
101 WHERE user_id = $1 AND action_type = $2 AND resolved_at IS NULL
102 ",
103 )
104 .bind(user_id)
105 .bind(action_type)
106 .execute(pool)
107 .await?;
108
109 Ok(result.rows_affected())
110 }
111
112 /// Resolve a content_removal action by content reference (item ID).
113 #[tracing::instrument(skip_all)]
114 pub(crate) async fn resolve_content_removal(pool: &PgPool, content_ref: &str) -> Result<u64> {
115 let result = sqlx::query(
116 r"
117 UPDATE moderation_actions
118 SET resolved_at = NOW()
119 WHERE action_type = 'content_removal' AND content_ref = $1 AND resolved_at IS NULL
120 ",
121 )
122 .bind(content_ref)
123 .execute(pool)
124 .await?;
125
126 Ok(result.rows_affected())
127 }
128