max / makenotwork
106 files changed,
+716 insertions,
-862 deletions
| @@ -107,3 +107,38 @@ pom-contract = { path = "../shared/pom-contract" } | |||
| 107 | 107 | # Enable mt-db's setup-only mutations for the integration suite. Resolver 2 keeps | |
| 108 | 108 | # this feature out of the normal/production build. | |
| 109 | 109 | mt-db = { workspace = true, features = ["test-support"] } | |
| 110 | + | ||
| 111 | + | [workspace.lints.rust] | |
| 112 | + | unused = "warn" | |
| 113 | + | unreachable_pub = "warn" | |
| 114 | + | ||
| 115 | + | [workspace.lints.clippy] | |
| 116 | + | pedantic = { level = "warn", priority = -1 } | |
| 117 | + | # Allow-list tuned from a measured breakdown across server/multithreaded/pter | |
| 118 | + | # (2026-07-22). These are the high-churn / low-signal pedantic lints; everything | |
| 119 | + | # else in `pedantic` stays a warning. Keep this block identical across repos. | |
| 120 | + | module_name_repetitions = "allow" | |
| 121 | + | # Doc lints. No docs-completeness push is underway. | |
| 122 | + | missing_errors_doc = "allow" | |
| 123 | + | missing_panics_doc = "allow" | |
| 124 | + | doc_markdown = "allow" | |
| 125 | + | # Numeric casts. Endemic and mostly intentional in size and byte math. | |
| 126 | + | cast_possible_truncation = "allow" | |
| 127 | + | cast_sign_loss = "allow" | |
| 128 | + | cast_precision_loss = "allow" | |
| 129 | + | cast_possible_wrap = "allow" | |
| 130 | + | cast_lossless = "allow" | |
| 131 | + | # Subjective structure and style nags. High churn, low signal. | |
| 132 | + | must_use_candidate = "allow" | |
| 133 | + | too_many_lines = "allow" | |
| 134 | + | struct_excessive_bools = "allow" | |
| 135 | + | similar_names = "allow" | |
| 136 | + | items_after_statements = "allow" | |
| 137 | + | single_match_else = "allow" | |
| 138 | + | # Frequent false-positives in TUI and router-heavy code. | |
| 139 | + | match_same_arms = "allow" | |
| 140 | + | unnecessary_wraps = "allow" | |
| 141 | + | type_complexity = "allow" | |
| 142 | + | ||
| 143 | + | [lints] | |
| 144 | + | workspace = true |
| @@ -18,7 +18,7 @@ fn main() { | |||
| 18 | 18 | /// One global version rather than a per-file hash: an upgrade to any watched | |
| 19 | 19 | /// file re-fetches all of them, which costs a handful of requests once and | |
| 20 | 20 | /// keeps the templates free of per-asset bookkeeping. What it buys is that a | |
| 21 | - | /// redeployed file can never be served stale from browser cache — for a chat | |
| 21 | + | /// redeployed file can never be served stale from browser cache, for a chat | |
| 22 | 22 | /// island that would mean old protocol logic talking to a new server, which | |
| 23 | 23 | /// presents as "chat is broken for some people" rather than as a cache bug. | |
| 24 | 24 | /// | |
| @@ -66,9 +66,7 @@ fn fingerprint_assets() { | |||
| 66 | 66 | ||
| 67 | 67 | /// Write `contents` to `path` only if it differs, to avoid needless rebuilds. | |
| 68 | 68 | fn write_if_changed(path: &Path, contents: &str) { | |
| 69 | - | let needs_write = fs::read_to_string(path) | |
| 70 | - | .map(|existing| existing != contents) | |
| 71 | - | .unwrap_or(true); | |
| 69 | + | let needs_write = fs::read_to_string(path).map_or(true, |existing| existing != contents); | |
| 72 | 70 | if needs_write { | |
| 73 | 71 | fs::write(path, contents) | |
| 74 | 72 | .unwrap_or_else(|e| panic!("failed to write {}: {e}", path.display())); | |
| @@ -76,7 +74,7 @@ fn write_if_changed(path: &Path, contents: &str) { | |||
| 76 | 74 | } | |
| 77 | 75 | ||
| 78 | 76 | /// Recursively hash every `.js` file under `dir` into `hasher`, in a | |
| 79 | - | /// deterministic order. A missing directory is a no-op — the first build on a | |
| 77 | + | /// deterministic order. A missing directory is a no-op, the first build on a | |
| 80 | 78 | /// fresh checkout runs before the frontend has been compiled. | |
| 81 | 79 | fn hash_dir_js(dir: &Path, hasher: &mut DefaultHasher) { | |
| 82 | 80 | let Ok(entries) = fs::read_dir(dir) else { |
| @@ -7,3 +7,6 @@ edition.workspace = true | |||
| 7 | 7 | chrono = { workspace = true } | |
| 8 | 8 | serde = { workspace = true } | |
| 9 | 9 | sqlx = { workspace = true } | |
| 10 | + | ||
| 11 | + | [lints] | |
| 12 | + | workspace = true |
| @@ -4,9 +4,7 @@ use serde::Serialize; | |||
| 4 | 4 | use sqlx::types::Uuid; | |
| 5 | 5 | use std::fmt; | |
| 6 | 6 | ||
| 7 | - | // ============================================================================ | |
| 8 | - | // CommunityRole — owner, moderator, member | |
| 9 | - | // ============================================================================ | |
| 7 | + | // CommunityRole, owner, moderator, member | |
| 10 | 8 | ||
| 11 | 9 | /// Role a user holds within a community. | |
| 12 | 10 | #[derive(Debug, Clone, Copy, PartialEq, Eq, sqlx::Type)] | |
| @@ -58,9 +56,7 @@ impl fmt::Display for CommunityRole { | |||
| 58 | 56 | } | |
| 59 | 57 | } | |
| 60 | 58 | ||
| 61 | - | // ============================================================================ | |
| 62 | - | // CommunityState — moderation state machine | |
| 63 | - | // ============================================================================ | |
| 59 | + | // CommunityState, moderation state machine | |
| 64 | 60 | ||
| 65 | 61 | /// Community-level moderation state. Distinct from platform suspension | |
| 66 | 62 | /// (`communities.suspended_at`); this is what owners/mods control. | |
| @@ -130,9 +126,7 @@ impl fmt::Display for CommunityState { | |||
| 130 | 126 | } | |
| 131 | 127 | } | |
| 132 | 128 | ||
| 133 | - | // ============================================================================ | |
| 134 | - | // BanType — ban or mute | |
| 135 | - | // ============================================================================ | |
| 129 | + | // BanType, ban or mute | |
| 136 | 130 | ||
| 137 | 131 | /// Type of community restriction. | |
| 138 | 132 | #[derive(Debug, Clone, Copy, PartialEq, Eq, sqlx::Type)] | |
| @@ -157,9 +151,7 @@ impl fmt::Display for BanType { | |||
| 157 | 151 | } | |
| 158 | 152 | } | |
| 159 | 153 | ||
| 160 | - | // ============================================================================ | |
| 161 | - | // ModAction — actions recorded in the mod log | |
| 162 | - | // ============================================================================ | |
| 154 | + | // ModAction, actions recorded in the mod log | |
| 163 | 155 | ||
| 164 | 156 | /// Action recorded in the moderation log. | |
| 165 | 157 | #[derive(Debug, Clone, Copy, PartialEq, Eq, sqlx::Type)] | |
| @@ -221,7 +213,7 @@ impl ModAction { | |||
| 221 | 213 | /// Who performed a moderation action, for attribution in the mod log. | |
| 222 | 214 | /// | |
| 223 | 215 | /// A `System` action (e.g. flag-threshold auto-hide) is recorded with a NULL | |
| 224 | - | /// `actor_id`, mirroring the `posts.removed_by IS NULL` convention — never the | |
| 216 | + | /// `actor_id`, mirroring the `posts.removed_by IS NULL` convention, never the | |
| 225 | 217 | /// user who happened to trip the threshold. `User` carries the moderator's | |
| 226 | 218 | /// `mnw_account_id`. | |
| 227 | 219 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] | |
| @@ -246,9 +238,7 @@ impl fmt::Display for ModAction { | |||
| 246 | 238 | } | |
| 247 | 239 | } | |
| 248 | 240 | ||
| 249 | - | // ============================================================================ | |
| 250 | - | // SortOrder — ascending or descending | |
| 251 | - | // ============================================================================ | |
| 241 | + | // SortOrder, ascending or descending | |
| 252 | 242 | ||
| 253 | 243 | /// Sort direction for paginated listings. | |
| 254 | 244 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] | |
| @@ -280,9 +270,7 @@ impl fmt::Display for SortOrder { | |||
| 280 | 270 | } | |
| 281 | 271 | } | |
| 282 | 272 | ||
| 283 | - | // ============================================================================ | |
| 284 | - | // SortColumn — which column to sort threads by | |
| 285 | - | // ============================================================================ | |
| 273 | + | // SortColumn, which column to sort threads by | |
| 286 | 274 | ||
| 287 | 275 | /// Column used to sort thread listings. | |
| 288 | 276 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
| @@ -15,3 +15,6 @@ chrono = { workspace = true } | |||
| 15 | 15 | uuid = { workspace = true } | |
| 16 | 16 | tracing = { workspace = true } | |
| 17 | 17 | serde = { workspace = true } | |
| 18 | + | ||
| 19 | + | [lints] | |
| 20 | + | workspace = true |
| @@ -1,4 +1,4 @@ | |||
| 1 | - | //! Database access layer — queries and mutations for Multithreaded. | |
| 1 | + | //! Database access layer: queries and mutations for Multithreaded. | |
| 2 | 2 | ||
| 3 | 3 | pub mod mutations; | |
| 4 | 4 | pub mod queries; |
| @@ -1,4 +1,4 @@ | |||
| 1 | - | use super::*; | |
| 1 | + | use super::{PgPool, Uuid}; | |
| 2 | 2 | ||
| 3 | 3 | /// Create a new category in a community. | |
| 4 | 4 | #[tracing::instrument(skip_all)] |
| @@ -1,4 +1,4 @@ | |||
| 1 | - | use super::*; | |
| 1 | + | use super::{CommunityState, PgPool, Utc, Uuid}; | |
| 2 | 2 | ||
| 3 | 3 | /// Create a new community and return its ID. | |
| 4 | 4 | #[tracing::instrument(skip_all)] | |
| @@ -96,7 +96,7 @@ pub struct CleanSlateResult { | |||
| 96 | 96 | /// then inserted). Useful for the success toast. | |
| 97 | 97 | pub deleted_thread_count: i64, | |
| 98 | 98 | /// ID of the system "Community reset" thread created in the first | |
| 99 | - | /// category. `None` if the community has no categories — clean-slate | |
| 99 | + | /// category. `None` if the community has no categories, clean-slate | |
| 100 | 100 | /// still deletes threads but has nowhere to post the notice. | |
| 101 | 101 | pub system_thread_id: Option<Uuid>, | |
| 102 | 102 | } | |
| @@ -132,7 +132,7 @@ pub async fn clean_slate_community( | |||
| 132 | 132 | .await?; | |
| 133 | 133 | ||
| 134 | 134 | // Pick the first category by sort_order to host the reset notice. None | |
| 135 | - | // means the community has no categories — nothing to post into. | |
| 135 | + | // means the community has no categories, nothing to post into. | |
| 136 | 136 | let first_category = sqlx::query_scalar!( | |
| 137 | 137 | "SELECT id FROM categories | |
| 138 | 138 | WHERE community_id = $1 |
| @@ -1,4 +1,4 @@ | |||
| 1 | - | use super::*; | |
| 1 | + | use super::{PgPool, Uuid}; | |
| 2 | 2 | ||
| 3 | 3 | /// Toggle endorsement: insert if missing, delete if exists. Returns true if now endorsed. | |
| 4 | 4 | /// Uses a transaction to prevent race conditions between concurrent toggle requests. | |
| @@ -19,7 +19,7 @@ pub async fn toggle_endorsement( | |||
| 19 | 19 | .await?; | |
| 20 | 20 | ||
| 21 | 21 | if result.rows_affected() == 0 { | |
| 22 | - | // Already existed — remove it | |
| 22 | + | // Already existed; remove it | |
| 23 | 23 | sqlx::query!( | |
| 24 | 24 | "DELETE FROM post_endorsements WHERE post_id = $1 AND endorser_id = $2", | |
| 25 | 25 | post_id, |
| @@ -1,4 +1,4 @@ | |||
| 1 | - | use super::*; | |
| 1 | + | use super::{PgPool, Uuid}; | |
| 2 | 2 | ||
| 3 | 3 | /// Insert a footnote on a post. Returns the footnote ID. | |
| 4 | 4 | #[tracing::instrument(skip_all)] |
| @@ -1,4 +1,4 @@ | |||
| 1 | - | use super::*; | |
| 1 | + | use super::{PgPool, Uuid}; | |
| 2 | 2 | ||
| 3 | 3 | /// Insert an uploaded image record. | |
| 4 | 4 | #[tracing::instrument(skip_all)] | |
| @@ -44,8 +44,8 @@ pub async fn remove_image<'e, E: sqlx::PgExecutor<'e>>( | |||
| 44 | 44 | } | |
| 45 | 45 | ||
| 46 | 46 | /// Hard-delete an image row. Used to roll back the row inserted just before a | |
| 47 | - | /// failed S3 upload — the object never landed in the bucket, so there is | |
| 48 | - | /// nothing to reconcile and the row should simply not exist. | |
| 47 | + | /// failed S3 upload, the object never landed in the bucket, so there is | |
| 48 | + | /// nothing to reconcile and the row should not exist. | |
| 49 | 49 | #[tracing::instrument(skip_all)] | |
| 50 | 50 | pub async fn delete_image_row(pool: &PgPool, image_id: Uuid) -> Result<(), sqlx::Error> { | |
| 51 | 51 | sqlx::query!("DELETE FROM images WHERE id = $1", image_id) |
| @@ -1,4 +1,4 @@ | |||
| 1 | - | use super::*; | |
| 1 | + | use super::{PgPool, Uuid}; | |
| 2 | 2 | ||
| 3 | 3 | /// Insert a link preview for a post. Ignores duplicates. | |
| 4 | 4 | #[tracing::instrument(skip_all)] |
| @@ -1,4 +1,4 @@ | |||
| 1 | - | use super::*; | |
| 1 | + | use super::{CommunityRole, PgPool, Uuid}; | |
| 2 | 2 | ||
| 3 | 3 | /// Ensure a user has a membership in a community with the given role. | |
| 4 | 4 | /// Creates membership if none exists, does nothing if already a member. |
| @@ -1,4 +1,6 @@ | |||
| 1 | - | use super::*; | |
| 1 | + | use std::fmt::Write as _; | |
| 2 | + | ||
| 3 | + | use super::Uuid; | |
| 2 | 4 | ||
| 3 | 5 | /// Insert mention rows for a post (batch insert). Ignores duplicates. | |
| 4 | 6 | #[tracing::instrument(skip_all)] | |
| @@ -17,7 +19,7 @@ pub async fn insert_mentions<'e, E: sqlx::PgExecutor<'e>>( | |||
| 17 | 19 | if i > 0 { | |
| 18 | 20 | sql.push_str(", "); | |
| 19 | 21 | } | |
| 20 | - | sql.push_str(&format!("($1, ${})", i + 2)); | |
| 22 | + | let _ = write!(sql, "($1, ${})", i + 2); | |
| 21 | 23 | } | |
| 22 | 24 | sql.push_str(" ON CONFLICT DO NOTHING"); | |
| 23 | 25 | let mut query = sqlx::query(&sql).bind(post_id); |
| @@ -1,4 +1,4 @@ | |||
| 1 | - | //! Database write mutations — inserts, updates, deletes. | |
| 1 | + | //! Database write mutations, inserts, updates, deletes. | |
| 2 | 2 | ||
| 3 | 3 | use chrono::{DateTime, Utc}; | |
| 4 | 4 | use mt_core::types::{BanType, CommunityRole, CommunityState, ModAction, ModActor}; |
| @@ -1,4 +1,4 @@ | |||
| 1 | - | use super::*; | |
| 1 | + | use super::{BanType, DateTime, ModAction, ModActor, PgPool, Utc, Uuid}; | |
| 2 | 2 | ||
| 3 | 3 | /// Create or update a ban/mute. Returns the ban ID. | |
| 4 | 4 | #[tracing::instrument(skip_all)] | |
| @@ -71,7 +71,7 @@ pub async fn remove_community_ban<'e, E: sqlx::PgExecutor<'e>>( | |||
| 71 | 71 | /// Insert a mod log entry on the given executor. | |
| 72 | 72 | /// | |
| 73 | 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 | |
| 74 | + | /// transaction as the mutation it records: handlers open one `tx`, run the | |
| 75 | 75 | /// mutation and this insert on `&mut *tx`, then commit, so an auditable action | |
| 76 | 76 | /// can never commit without its log row. A `System` actor persists as a NULL | |
| 77 | 77 | /// `actor_id` (migration 032). |
| @@ -1,11 +1,11 @@ | |||
| 1 | - | use super::*; | |
| 1 | + | use super::{PgPool, Uuid}; | |
| 2 | 2 | ||
| 3 | 3 | /// Insert a reply and bump the thread's last_activity_at atomically. | |
| 4 | 4 | /// | |
| 5 | 5 | /// Opening posts are created with the thread via [`create_thread_with_op`]; | |
| 6 | 6 | /// this is for replies only. The display reply count derives from the | |
| 7 | 7 | /// denormalized `threads.post_count`, which the m029 trigger maintains on every | |
| 8 | - | /// post INSERT/DELETE/removed_at change — no counter to update by hand here. | |
| 8 | + | /// post INSERT/DELETE/removed_at change, no counter to update by hand here. | |
| 9 | 9 | #[tracing::instrument(skip_all)] | |
| 10 | 10 | pub async fn create_post( | |
| 11 | 11 | pool: &PgPool, | |
| @@ -40,7 +40,7 @@ pub async fn create_post_tx( | |||
| 40 | 40 | body_html: &str, | |
| 41 | 41 | ) -> Result<Option<Uuid>, sqlx::Error> { | |
| 42 | 42 | // Insert only while the thread is still live and unlocked, in the same | |
| 43 | - | // statement that reads that state — closing the TOCTOU between a handler's | |
| 43 | + | // statement that reads that state, closing the TOCTOU between a handler's | |
| 44 | 44 | // snapshot-time `locked`/`deleted_at` check and this insert. Without the | |
| 45 | 45 | // guard a concurrent lock or soft-delete in the gap would let the reply | |
| 46 | 46 | // commit (and the m029 post_count trigger increment a deleted thread). None | |
| @@ -81,7 +81,7 @@ pub async fn create_post_tx( | |||
| 81 | 81 | /// server→server reply carrying the same `external_ref` inserts at most once. | |
| 82 | 82 | /// `ON CONFLICT` on the partial unique index (m030) makes the repeat a no-op, | |
| 83 | 83 | /// so the m029 post_count trigger fires exactly once and `last_activity_at` is | |
| 84 | - | /// bumped only on the fresh insert. Returns `(post_id, created)` — `created` is | |
| 84 | + | /// bumped only on the fresh insert. Returns `(post_id, created)`, `created` is | |
| 85 | 85 | /// false when an existing post was returned. User-facing replies use the plain | |
| 86 | 86 | /// [`create_post`] (no ref); this path is internal-only. | |
| 87 | 87 | #[tracing::instrument(skip_all)] | |
| @@ -137,7 +137,7 @@ pub async fn create_post_external_ref( | |||
| 137 | 137 | /// Test-only: mod-remove a single post without the OP-cascade. | |
| 138 | 138 | /// | |
| 139 | 139 | /// Production code must use [`mod_remove_post_cascade`], which also soft-deletes | |
| 140 | - | /// the thread when the post is its opening post — removing an OP without the | |
| 140 | + | /// the thread when the post is its opening post, removing an OP without the | |
| 141 | 141 | /// cascade leaves a headless, still-repliable thread. This non-cascade variant | |
| 142 | 142 | /// exists solely so the integration suite can stage a removed post directly; it | |
| 143 | 143 | /// is gated behind the `test-support` feature so handler code cannot reach it. | |
| @@ -177,7 +177,7 @@ pub struct PostRemoval { | |||
| 177 | 177 | /// Removing an OP without this cascade would leave a headless, still-repliable | |
| 178 | 178 | /// thread; cascading deletes it so listings, the thread view, and the reply | |
| 179 | 179 | /// path all treat it as gone. Returns false flags when the post was already | |
| 180 | - | /// removed or was not the OP — callers can log a thread-deletion accordingly. | |
| 180 | + | /// removed or was not the OP, callers can log a thread-deletion accordingly. | |
| 181 | 181 | #[tracing::instrument(skip_all)] | |
| 182 | 182 | pub async fn mod_remove_post_cascade( | |
| 183 | 183 | conn: &mut sqlx::PgConnection, |
| @@ -1,4 +1,6 @@ | |||
| 1 | - | use super::*; | |
| 1 | + | use std::fmt::Write as _; | |
| 2 | + | ||
| 3 | + | use super::{PgPool, Uuid}; | |
| 2 | 4 | ||
| 3 | 5 | /// Create a tag in a community. Returns the tag ID. | |
| 4 | 6 | #[tracing::instrument(skip_all)] | |
| @@ -69,7 +71,7 @@ pub async fn set_thread_tags_tx( | |||
| 69 | 71 | sql.push_str(", "); | |
| 70 | 72 | } | |
| 71 | 73 | // $1 = thread_id, $2.. = tag_ids | |
| 72 | - | sql.push_str(&format!("($1, ${})", i + 2)); | |
| 74 | + | let _ = write!(sql, "($1, ${})", i + 2); | |
| 73 | 75 | } | |
| 74 | 76 | let mut query = sqlx::query(&sql).bind(thread_id); | |
| 75 | 77 | for tag_id in tag_ids { |
| @@ -1,4 +1,4 @@ | |||
| 1 | - | use super::*; | |
| 1 | + | use super::{PgPool, Uuid}; | |
| 2 | 2 | ||
| 3 | 3 | /// Atomically create an externally-referenced thread and its opening post in | |
| 4 | 4 | /// one transaction. Returns `(thread_id, op_post_id)`. | |
| @@ -48,7 +48,7 @@ pub async fn create_thread_with_op_external_ref( | |||
| 48 | 48 | ||
| 49 | 49 | /// Low-level: insert a bare thread row (no opening post). Returns the thread ID. | |
| 50 | 50 | /// | |
| 51 | - | /// Handlers must NOT pair this with a separate `create_post` for the OP — that | |
| 51 | + | /// Handlers must NOT pair this with a separate `create_post` for the OP, that | |
| 52 | 52 | /// non-atomic sequence can leak a post-less thread. Use [`create_thread_with_op`] | |
| 53 | 53 | /// for the real "start a thread" operation. This primitive exists for tests and | |
| 54 | 54 | /// data construction that build posts explicitly. | |
| @@ -143,7 +143,7 @@ pub async fn create_thread_with_op_tx( | |||
| 143 | 143 | /// Atomically auto-hide a post if pending flag count meets the threshold. | |
| 144 | 144 | /// Combines count check and removal in a single query to avoid race conditions. | |
| 145 | 145 | /// Returns true if the post was actually removed. | |
| 146 | - | /// Sets removed_by to NULL (system action) — the mod log records the event. | |
| 146 | + | /// Sets removed_by to NULL (system action), the mod log records the event. | |
| 147 | 147 | #[tracing::instrument(skip_all)] | |
| 148 | 148 | pub async fn auto_hide_if_threshold_met<'e, E: sqlx::PgExecutor<'e>>( | |
| 149 | 149 | executor: E, |
| @@ -1,4 +1,4 @@ | |||
| 1 | - | use super::*; | |
| 1 | + | use super::{PgPool, Uuid}; | |
| 2 | 2 | ||
| 3 | 3 | /// Track a thread (upsert). | |
| 4 | 4 | #[tracing::instrument(skip_all)] |