//! Notifications, watching, subscriptions and the search index (hub spec //! §10, §11). use super::{now_ms, Pool}; use sqlx::FromRow; #[derive(Clone, Debug, FromRow)] pub struct Notification { pub id: i64, pub account_id: i64, pub at_ms: i64, pub kind: String, pub title: String, pub url: String, pub actor: String, pub read: bool, } pub async fn add(pool: &Pool, account_id: i64, kind: &str, title: &str, url: &str, actor: &str) -> sqlx::Result<()> { sqlx::query("INSERT INTO notifications (account_id, at_ms, kind, title, url, actor) VALUES (?, ?, ?, ?, ?, ?)") .bind(account_id) .bind(now_ms()) .bind(kind) .bind(title) .bind(url) .bind(actor) .execute(pool) .await?; Ok(()) } pub async fn inbox(pool: &Pool, account_id: i64, limit: i64) -> sqlx::Result> { sqlx::query_as("SELECT * FROM notifications WHERE account_id = ? ORDER BY read, at_ms DESC, id DESC LIMIT ?") .bind(account_id) .bind(limit) .fetch_all(pool) .await } pub async fn unread(pool: &Pool, account_id: i64) -> sqlx::Result { sqlx::query_scalar("SELECT COUNT(*) FROM notifications WHERE account_id = ? AND read = 0").bind(account_id).fetch_one(pool).await } pub async fn mark_read(pool: &Pool, account_id: i64, id: Option) -> sqlx::Result<()> { match id { Some(id) => { sqlx::query("UPDATE notifications SET read = 1 WHERE account_id = ? AND id = ?").bind(account_id).bind(id).execute(pool).await? } None => sqlx::query("UPDATE notifications SET read = 1 WHERE account_id = ?").bind(account_id).execute(pool).await?, }; Ok(()) } // ---------------------------------------------------------------- watching pub async fn watching(pool: &Pool, account_id: i64, repo_id: i64) -> sqlx::Result { let n: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM watches WHERE account_id = ? AND repo_id = ?") .bind(account_id) .bind(repo_id) .fetch_one(pool) .await?; Ok(n > 0) } pub async fn set_watch(pool: &Pool, account_id: i64, repo_id: i64, on: bool) -> sqlx::Result<()> { if on { sqlx::query("INSERT OR IGNORE INTO watches (account_id, repo_id) VALUES (?, ?)") .bind(account_id) .bind(repo_id) .execute(pool) .await?; } else { sqlx::query("DELETE FROM watches WHERE account_id = ? AND repo_id = ?").bind(account_id).bind(repo_id).execute(pool).await?; } Ok(()) } pub async fn watchers(pool: &Pool, repo_id: i64) -> sqlx::Result> { sqlx::query_scalar("SELECT account_id FROM watches WHERE repo_id = ?").bind(repo_id).fetch_all(pool).await } pub async fn subscribe(pool: &Pool, account_id: i64, kind: &str, target_id: i64) -> sqlx::Result<()> { sqlx::query("INSERT OR IGNORE INTO subscriptions (account_id, kind, target_id) VALUES (?, ?, ?)") .bind(account_id) .bind(kind) .bind(target_id) .execute(pool) .await?; Ok(()) } pub async fn unsubscribe(pool: &Pool, account_id: i64, kind: &str, target_id: i64) -> sqlx::Result<()> { sqlx::query("DELETE FROM subscriptions WHERE account_id = ? AND kind = ? AND target_id = ?") .bind(account_id) .bind(kind) .bind(target_id) .execute(pool) .await?; Ok(()) } pub async fn subscribed(pool: &Pool, account_id: i64, kind: &str, target_id: i64) -> sqlx::Result { let n: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM subscriptions WHERE account_id = ? AND kind = ? AND target_id = ?") .bind(account_id) .bind(kind) .bind(target_id) .fetch_one(pool) .await?; Ok(n > 0) } pub async fn subscribers(pool: &Pool, kind: &str, target_id: i64) -> sqlx::Result> { sqlx::query_scalar("SELECT account_id FROM subscriptions WHERE kind = ? AND target_id = ?") .bind(kind) .bind(target_id) .fetch_all(pool) .await } // ------------------------------------------------------------------ search #[derive(Clone, Debug, FromRow)] pub struct Hit { pub kind: String, pub repo_id: i64, pub target_id: i64, pub number: i64, pub title: String, pub snippet: String, } /// Replace the indexed text of one thread (title plus body plus comments). pub async fn index(pool: &Pool, kind: &str, repo_id: i64, target_id: i64, number: i64, title: &str, body: &str) -> sqlx::Result<()> { sqlx::query("DELETE FROM search WHERE kind = ? AND target_id = ?").bind(kind).bind(target_id).execute(pool).await?; sqlx::query("INSERT INTO search (kind, repo_id, target_id, number, title, body) VALUES (?, ?, ?, ?, ?, ?)") .bind(kind) .bind(repo_id) .bind(target_id) .bind(number) .bind(title) .bind(body) .execute(pool) .await?; Ok(()) } /// Full-text hits, best first. The query is quoted so punctuation cannot /// reach the FTS5 parser. pub async fn search(pool: &Pool, query: &str, repo_id: Option, limit: i64) -> sqlx::Result> { let terms: Vec = query.split_whitespace().map(|t| format!("\"{}\"", t.replace('"', ""))).collect(); if terms.is_empty() { return Ok(vec![]); } let q = terms.join(" "); let sql = "SELECT kind, repo_id, target_id, number, title, snippet(search, 5, '', '', '…', 12) AS snippet FROM search WHERE search MATCH ? AND (? IS NULL OR repo_id = ?) ORDER BY rank LIMIT ?"; sqlx::query_as(sql).bind(q).bind(repo_id).bind(repo_id).bind(limit).fetch_all(pool).await }