Skip to main content

max / makenotwork

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