Skip to main content

max / makenotwork

4.9 KB · 170 lines History Blame Raw
1 //! bans, mod-log entries and flag resolution
2
3 use super::{BanType, DateTime, ModAction, ModActor, PgPool, Utc, Uuid};
4
5 /// Create or update a ban/mute. Returns the ban ID.
6 #[tracing::instrument(skip_all)]
7 pub async fn create_community_ban<'e, E: sqlx::PgExecutor<'e>>(
8 executor: E,
9 community_id: Uuid,
10 user_id: Uuid,
11 banned_by: Uuid,
12 ban_type: BanType,
13 reason: Option<&str>,
14 expires_at: Option<DateTime<Utc>>,
15 ) -> Result<Uuid, sqlx::Error> {
16 // runtime-checked: the full server build unifies sqlx's `time` and `chrono`
17 // features (the session store pulls in `time`), and with both on the
18 // compile-time macro infers the TIMESTAMPTZ bind parameter as
19 // `time::OffsetDateTime`, which a `chrono::DateTime<Utc>` won't satisfy.
20 // Output-column type can be overridden in the macro; a bind parameter's
21 // cannot. This is the one write path that binds a chrono timestamp, so it
22 // stays a runtime-checked query (chrono's Encode handles the bind directly).
23 sqlx::query_scalar::<_, Uuid>(
24 "INSERT INTO community_bans (community_id, user_id, banned_by, ban_type, reason, expires_at)
25 VALUES ($1, $2, $3, $4, $5, $6)
26 ON CONFLICT (community_id, user_id, ban_type) DO UPDATE
27 SET banned_by = $3, reason = $5, expires_at = $6, created_at = now()
28 RETURNING id",
29 )
30 .bind(community_id)
31 .bind(user_id)
32 .bind(banned_by)
33 .bind(ban_type.as_str())
34 .bind(reason)
35 .bind(expires_at)
36 .fetch_one(executor)
37 .await
38 }
39
40 /// Delete all bans/mutes whose expiration has passed.
41 #[tracing::instrument(skip_all)]
42 pub async fn cleanup_expired_bans(pool: &PgPool, community_id: Uuid) -> Result<u64, sqlx::Error> {
43 let result = sqlx::query!(
44 "DELETE FROM community_bans
45 WHERE community_id = $1 AND expires_at IS NOT NULL AND expires_at <= now()",
46 community_id,
47 )
48 .execute(pool)
49 .await?;
50 Ok(result.rows_affected())
51 }
52
53 /// Remove a ban or mute.
54 #[tracing::instrument(skip_all)]
55 pub async fn remove_community_ban<'e, E: sqlx::PgExecutor<'e>>(
56 executor: E,
57 community_id: Uuid,
58 user_id: Uuid,
59 ban_type: BanType,
60 ) -> Result<(), sqlx::Error> {
61 sqlx::query!(
62 "DELETE FROM community_bans
63 WHERE community_id = $1 AND user_id = $2 AND ban_type = $3",
64 community_id,
65 user_id,
66 ban_type.as_str(),
67 )
68 .execute(executor)
69 .await?;
70 Ok(())
71 }
72
73 /// Insert a mod log entry on the given executor.
74 ///
75 /// Generic over the executor so the audit row can be written on the *same*
76 /// transaction as the mutation it records: handlers open one `tx`, run the
77 /// mutation and this insert on `&mut *tx`, then commit, so an auditable action
78 /// can never commit without its log row. A `System` actor persists as a NULL
79 /// `actor_id` (migration 032).
80 #[tracing::instrument(skip_all)]
81 pub async fn insert_mod_log<'e, E>(
82 executor: E,
83 community_id: Option<Uuid>,
84 actor: ModActor,
85 action: ModAction,
86 target_user: Option<Uuid>,
87 target_id: Option<Uuid>,
88 reason: Option<&str>,
89 ) -> Result<(), sqlx::Error>
90 where
91 E: sqlx::PgExecutor<'e>,
92 {
93 sqlx::query!(
94 "INSERT INTO mod_log (community_id, actor_id, action, target_user, target_id, reason)
95 VALUES ($1, $2, $3, $4, $5, $6)",
96 community_id,
97 actor.id(),
98 action.as_str(),
99 target_user,
100 target_id,
101 reason,
102 )
103 .execute(executor)
104 .await?;
105 Ok(())
106 }
107
108 /// Insert a flag on a post. ON CONFLICT DO NOTHING (idempotent per user+post).
109 #[tracing::instrument(skip_all)]
110 pub async fn insert_flag(
111 pool: &PgPool,
112 post_id: Uuid,
113 flagger_id: Uuid,
114 reason: &str,
115 detail: Option<&str>,
116 ) -> Result<(), sqlx::Error> {
117 sqlx::query!(
118 "INSERT INTO post_flags (post_id, flagger_id, reason, detail)
119 VALUES ($1, $2, $3, $4)
120 ON CONFLICT (post_id, flagger_id) DO NOTHING",
121 post_id,
122 flagger_id,
123 reason,
124 detail,
125 )
126 .execute(pool)
127 .await?;
128 Ok(())
129 }
130
131 /// Resolve a single flag.
132 #[tracing::instrument(skip_all)]
133 pub async fn resolve_flag(
134 pool: &PgPool,
135 flag_id: Uuid,
136 resolved_by: Uuid,
137 resolution: &str,
138 ) -> Result<(), sqlx::Error> {
139 sqlx::query!(
140 "UPDATE post_flags SET resolved_at = now(), resolved_by = $2, resolution = $3
141 WHERE id = $1 AND resolved_at IS NULL",
142 flag_id,
143 resolved_by,
144 resolution,
145 )
146 .execute(pool)
147 .await?;
148 Ok(())
149 }
150
151 /// Resolve all unresolved flags for a given post.
152 #[tracing::instrument(skip_all)]
153 pub async fn resolve_all_flags_for_post<'e, E: sqlx::PgExecutor<'e>>(
154 executor: E,
155 post_id: Uuid,
156 resolved_by: Uuid,
157 resolution: &str,
158 ) -> Result<(), sqlx::Error> {
159 sqlx::query!(
160 "UPDATE post_flags SET resolved_at = now(), resolved_by = $2, resolution = $3
161 WHERE post_id = $1 AND resolved_at IS NULL",
162 post_id,
163 resolved_by,
164 resolution,
165 )
166 .execute(executor)
167 .await?;
168 Ok(())
169 }
170