Skip to main content

max / makenotwork

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