//! Database repositories for feeds, items, and busser state. //! //! Each repository wraps an SQLite connection pool and provides typed //! CRUD operations for a single table. use chrono::{DateTime, Utc}; use sqlx::SqlitePool; use bb_interface::{ErrorCategory, StructuredError}; use crate::TIMESTAMP_FMT; use crate::id_types::{BookmarkId, BusserStateId, FeedId, ItemId, QueryFeedId}; use crate::models::{ CreateBookmark, CreateFeed, CreateFeedItem, CreateQueryFeed, DbBookmark, DbBusserState, DbFeed, DbFeedItem, DbQueryFeed, QueryCondition, UpdateBookmark, }; /// Sanitize a user-provided search string for FTS5 MATCH syntax. /// /// Wraps each word in double quotes to prevent FTS5 syntax injection /// (e.g. a user typing `AND`, `OR`, `NOT`, `NEAR`, or `NEAR/N` won't be /// interpreted as FTS5 operators). Words are joined with spaces (implicit AND). /// /// Inside quoted strings FTS5 still honours two special characters: /// - `*` at the end of the last token triggers prefix matching /// - `^` at the start of the first token triggers beginning-of-column matching /// /// We strip those so user input like `^hello` or `world*` is treated literally. /// The `column:` prefix syntax (e.g. `title:word`) is neutralised by the quoting /// itself, colons inside double-quoted strings are treated as literal characters. fn sanitize_fts_query(query: &str) -> String { query .split_whitespace() .map(|word| { // Escape embedded double quotes (FTS5 uses "" to represent a literal ") let escaped = word.replace('"', "\"\""); // Strip `^` prefix and `*` suffix, both are special inside FTS5 quotes let escaped = escaped.trim_start_matches('^'); let escaped = escaped.trim_end_matches('*'); format!("\"{escaped}\"") }) // Drop tokens that became empty after stripping (e.g. bare `^`, `*`, `^*`) .filter(|token| token != "\"\"") .collect::>() .join(" ") } /// Maximum allowed length for a search query string. /// /// Queries longer than this are rejected early (returning an empty result set) /// to prevent excessively large FTS5 MATCH expressions from consuming memory /// or CPU in SQLite. const MAX_SEARCH_QUERY_LENGTH: usize = 500; /// Number of consecutive failures before a feed is automatically disabled. /// /// Once a feed accumulates this many failures without a successful fetch, /// the circuit breaker trips: the feed is marked `circuit_broken = 1` and /// excluded from automatic fetch scheduling until manually reset. pub const CIRCUIT_BREAKER_THRESHOLD: i64 = 10; mod bookmarks; mod config; mod feeds; mod items; mod query_feeds; mod state; mod tags; pub use bookmarks::*; pub use config::*; pub use feeds::*; pub use items::*; pub use query_feeds::*; pub use state::*; pub use tags::*; #[cfg(test)] mod tests;