9adc69d514 2026-03-25 1: use crate::{
9adc69d514 2026-03-25 2: Arc,
9adc69d514 2026-03-25 3: Mutex,
9adc69d514 2026-03-25 4: };
9adc69d514 2026-03-25 5:
b4af85e31b 2025-06-29 6: use std::{
b4af85e31b 2025-06-29 7: borrow::Cow,
b4af85e31b 2025-06-29 8: fmt,
b4af85e31b 2025-06-29 9: };
b4af85e31b 2025-06-29 10:
0340541002 2025-04-24 11: use chrono::{
0340541002 2025-04-24 12: DateTime,
0340541002 2025-04-24 13: FixedOffset,
0340541002 2025-04-24 14: Local,
0340541002 2025-04-24 15: };
0340541002 2025-04-24 16: use sqlx::{
0340541002 2025-04-24 17: Postgres,
0340541002 2025-04-24 18: Row,
0340541002 2025-04-24 19: postgres::PgPoolOptions,
0340541002 2025-04-24 20: pool::PoolConnection,
44575a91d3 2025-07-09 21: };
44575a91d3 2025-07-09 22: use stacked_errors::{
44575a91d3 2025-07-09 23: Result,
44575a91d3 2025-07-09 24: StackableErr,
44575a91d3 2025-07-09 25: bail,
b4af85e31b 2025-06-29 26: };
0340541002 2025-04-24 27:
0340541002 2025-04-24 28: #[derive(sqlx::FromRow, Debug)]
0340541002 2025-04-24 29: pub struct List {
e3f7eeb26a 2025-04-26 30: pub source_id: i32,
0340541002 2025-04-24 31: pub channel: String,
0340541002 2025-04-24 32: pub enabled: bool,
0340541002 2025-04-24 33: pub url: String,
0340541002 2025-04-24 34: pub iv_hash: Option<String>,
0340541002 2025-04-24 35: pub url_re: Option<String>,
b4af85e31b 2025-06-29 36: }
b4af85e31b 2025-06-29 37:
b4af85e31b 2025-06-29 38: impl fmt::Display for List {
44575a91d3 2025-07-09 39: fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::result::Result<(), fmt::Error> {
3fd8c40aa8 2026-03-30 40: write!(f, "#feed_{} *ļøā£ <code>{}</code> {}\nš <code>{}</code>", self.source_id, self.channel,
b4af85e31b 2025-06-29 41: match self.enabled {
b4af85e31b 2025-06-29 42: true => "š enabled",
b4af85e31b 2025-06-29 43: false => "ā disabled",
b4af85e31b 2025-06-29 44: }, self.url)?;
b4af85e31b 2025-06-29 45: if let Some(iv_hash) = &self.iv_hash {
3fd8c40aa8 2026-03-30 46: write!(f, "\nIV: <code>{iv_hash}</code>")?;
b4af85e31b 2025-06-29 47: }
b4af85e31b 2025-06-29 48: if let Some(url_re) = &self.url_re {
3fd8c40aa8 2026-03-30 49: write!(f, "\nRE: <code>{url_re}</code>")?;
b4af85e31b 2025-06-29 50: }
b4af85e31b 2025-06-29 51: Ok(())
b4af85e31b 2025-06-29 52: }
9adc69d514 2026-03-25 53: }
9adc69d514 2026-03-25 54:
9adc69d514 2026-03-25 55: /// One feed, used for caching and menu navigation
9adc69d514 2026-03-25 56: #[derive(sqlx::FromRow, Debug)]
9adc69d514 2026-03-25 57: pub struct Feed {
9adc69d514 2026-03-25 58: pub source_id: i32,
9adc69d514 2026-03-25 59: pub channel: String,
0340541002 2025-04-24 60: }
0340541002 2025-04-24 61:
0340541002 2025-04-24 62: #[derive(sqlx::FromRow, Debug)]
0340541002 2025-04-24 63: pub struct Source {
0340541002 2025-04-24 64: pub channel_id: i64,
0340541002 2025-04-24 65: pub url: String,
0340541002 2025-04-24 66: pub iv_hash: Option<String>,
0340541002 2025-04-24 67: pub owner: i64,
0340541002 2025-04-24 68: pub url_re: Option<String>,
0340541002 2025-04-24 69: }
0340541002 2025-04-24 70:
0340541002 2025-04-24 71: #[derive(sqlx::FromRow)]
0340541002 2025-04-24 72: pub struct Queue {
0340541002 2025-04-24 73: pub source_id: Option<i32>,
0340541002 2025-04-24 74: pub next_fetch: Option<DateTime<Local>>,
0340541002 2025-04-24 75: pub owner: Option<i64>,
acb0a4ac54 2025-09-28 76: pub last_scrape: DateTime<Local>,
bb89b6fab8 2025-06-15 77: }
bb89b6fab8 2025-06-15 78:
bb89b6fab8 2025-06-15 79: #[derive(Clone)]
c6d3e97290 2025-07-01 80: pub struct Db (
c6d3e97290 2025-07-01 81: Arc<Mutex<sqlx::Pool<sqlx::Postgres>>>,
c6d3e97290 2025-07-01 82: );
bb89b6fab8 2025-06-15 83:
bb89b6fab8 2025-06-15 84: impl Db {
bb89b6fab8 2025-06-15 85: pub fn new (pguri: &str) -> Result<Db> {
c6d3e97290 2025-07-01 86: Ok(Db (
c6d3e97290 2025-07-01 87: Arc::new(Mutex::new(PgPoolOptions::new()
bb89b6fab8 2025-06-15 88: .max_connections(5)
bb89b6fab8 2025-06-15 89: .acquire_timeout(std::time::Duration::new(300, 0))
bb89b6fab8 2025-06-15 90: .idle_timeout(std::time::Duration::new(60, 0))
44575a91d3 2025-07-09 91: .connect_lazy(pguri)
44575a91d3 2025-07-09 92: .stack()?)),
c6d3e97290 2025-07-01 93: ))
bb89b6fab8 2025-06-15 94: }
bb89b6fab8 2025-06-15 95:
bb89b6fab8 2025-06-15 96: pub async fn begin(&self) -> Result<Conn> {
c6d3e97290 2025-07-01 97: let pool = self.0.lock_arc().await;
44575a91d3 2025-07-09 98: let conn = Conn ( pool.acquire().await.stack()? );
bb89b6fab8 2025-06-15 99: Ok(conn)
bb89b6fab8 2025-06-15 100: }
bb89b6fab8 2025-06-15 101: }
bb89b6fab8 2025-06-15 102:
c6d3e97290 2025-07-01 103: pub struct Conn (
c6d3e97290 2025-07-01 104: PoolConnection<Postgres>,
c6d3e97290 2025-07-01 105: );
bb89b6fab8 2025-06-15 106:
c6d3e97290 2025-07-01 107: impl Conn {
bb89b6fab8 2025-06-15 108: pub async fn add_post (&mut self, source_id: i32, date: &DateTime<FixedOffset>, post_url: &str) -> Result<()> {
bb89b6fab8 2025-06-15 109: sqlx::query("insert into rsstg_post (source_id, posted, url) values ($1, $2, $3);")
bb89b6fab8 2025-06-15 110: .bind(source_id)
bb89b6fab8 2025-06-15 111: .bind(date)
bb89b6fab8 2025-06-15 112: .bind(post_url)
44575a91d3 2025-07-09 113: .execute(&mut *self.0).await.stack()?;
bb89b6fab8 2025-06-15 114: Ok(())
bb89b6fab8 2025-06-15 115: }
bb89b6fab8 2025-06-15 116:
bb89b6fab8 2025-06-15 117: pub async fn clean <I> (&mut self, source_id: i32, owner: I) -> Result<Cow<'_, str>>
bb89b6fab8 2025-06-15 118: where I: Into<i64> {
bb89b6fab8 2025-06-15 119: match sqlx::query("delete from rsstg_post p using rsstg_source s where p.source_id = $1 and owner = $2 and p.source_id = s.source_id;")
bb89b6fab8 2025-06-15 120: .bind(source_id)
bb89b6fab8 2025-06-15 121: .bind(owner.into())
44575a91d3 2025-07-09 122: .execute(&mut *self.0).await.stack()?.rows_affected() {
0340541002 2025-04-24 123: 0 => { Ok("No data found found.".into()) },
0340541002 2025-04-24 124: x => { Ok(format!("{x} posts purged.").into()) },
0340541002 2025-04-24 125: }
0340541002 2025-04-24 126: }
0340541002 2025-04-24 127:
bb89b6fab8 2025-06-15 128: pub async fn delete <I> (&mut self, source_id: i32, owner: I) -> Result<Cow<'_, str>>
bb89b6fab8 2025-06-15 129: where I: Into<i64> {
0340541002 2025-04-24 130: match sqlx::query("delete from rsstg_source where source_id = $1 and owner = $2;")
0340541002 2025-04-24 131: .bind(source_id)
bb89b6fab8 2025-06-15 132: .bind(owner.into())
44575a91d3 2025-07-09 133: .execute(&mut *self.0).await.stack()?.rows_affected() {
0340541002 2025-04-24 134: 0 => { Ok("No data found found.".into()) },
44575a91d3 2025-07-09 135: x => { Ok(format!("{x} sources removed.").into()) },
0340541002 2025-04-24 136: }
0340541002 2025-04-24 137: }
0340541002 2025-04-24 138:
bb89b6fab8 2025-06-15 139: pub async fn disable <I> (&mut self, source_id: i32, owner: I) -> Result<&str>
bb89b6fab8 2025-06-15 140: where I: Into<i64> {
0340541002 2025-04-24 141: match sqlx::query("update rsstg_source set enabled = false where source_id = $1 and owner = $2")
0340541002 2025-04-24 142: .bind(source_id)
bb89b6fab8 2025-06-15 143: .bind(owner.into())
44575a91d3 2025-07-09 144: .execute(&mut *self.0).await.stack()?.rows_affected() {
0340541002 2025-04-24 145: 1 => { Ok("Source disabled.") },
0340541002 2025-04-24 146: 0 => { Ok("Source not found.") },
0340541002 2025-04-24 147: _ => { bail!("Database error.") },
0340541002 2025-04-24 148: }
0340541002 2025-04-24 149: }
0340541002 2025-04-24 150:
bb89b6fab8 2025-06-15 151: pub async fn enable <I> (&mut self, source_id: i32, owner: I) -> Result<&str>
bb89b6fab8 2025-06-15 152: where I: Into<i64> {
0340541002 2025-04-24 153: match sqlx::query("update rsstg_source set enabled = true where source_id = $1 and owner = $2")
0340541002 2025-04-24 154: .bind(source_id)
bb89b6fab8 2025-06-15 155: .bind(owner.into())
44575a91d3 2025-07-09 156: .execute(&mut *self.0).await.stack()?.rows_affected() {
0340541002 2025-04-24 157: 1 => { Ok("Source enabled.") },
0340541002 2025-04-24 158: 0 => { Ok("Source not found.") },
0340541002 2025-04-24 159: _ => { bail!("Database error.") },
0340541002 2025-04-24 160: }
0340541002 2025-04-24 161: }
0340541002 2025-04-24 162:
fabcca1eaf 2026-01-09 163: /// Checks whether a post with the given URL exists for the specified source.
fabcca1eaf 2026-01-09 164: ///
fabcca1eaf 2026-01-09 165: /// # Parameters
fabcca1eaf 2026-01-09 166: /// - `post_url`: The URL of the post to check.
fabcca1eaf 2026-01-09 167: /// - `id`: The source identifier (converted to `i64`).
fabcca1eaf 2026-01-09 168: ///
fabcca1eaf 2026-01-09 169: /// # Returns
fabcca1eaf 2026-01-09 170: /// `true` if a post with the URL exists for the source, `false` otherwise.
7393d62235 2026-01-07 171: pub async fn exists <I> (&mut self, post_url: &str, id: I) -> Result<bool>
bb89b6fab8 2025-06-15 172: where I: Into<i64> {
0340541002 2025-04-24 173: let row = sqlx::query("select exists(select true from rsstg_post where url = $1 and source_id = $2) as exists;")
0340541002 2025-04-24 174: .bind(post_url)
bb89b6fab8 2025-06-15 175: .bind(id.into())
44575a91d3 2025-07-09 176: .fetch_one(&mut *self.0).await.stack()?;
fabcca1eaf 2026-01-09 177: row.try_get("exists")
fabcca1eaf 2026-01-09 178: .stack_err("Database error: can't check whether post exists.")
fabcca1eaf 2026-01-09 179: }
fabcca1eaf 2026-01-09 180:
9adc69d514 2026-03-25 181: pub async fn get_feeds <I>(&mut self, owner: I) -> Result<Vec<Feed>>
9adc69d514 2026-03-25 182: where I: Into<i64> {
9adc69d514 2026-03-25 183: let block: Vec<Feed> = sqlx::query_as("select source_id, channel from rsstg_source where owner = $1 order by source_id")
9adc69d514 2026-03-25 184: .bind(owner.into())
9adc69d514 2026-03-25 185: .fetch_all(&mut *self.0).await.stack()?;
9adc69d514 2026-03-25 186: Ok(block)
9adc69d514 2026-03-25 187: }
9adc69d514 2026-03-25 188:
7393d62235 2026-01-07 189: /// Get all pending events for (now + 1 minute)
0340541002 2025-04-24 190: pub async fn get_queue (&mut self) -> Result<Vec<Queue>> {
acb0a4ac54 2025-09-28 191: let block: Vec<Queue> = sqlx::query_as("select source_id, next_fetch, owner, last_scrape from rsstg_order natural left join rsstg_source where next_fetch < now() + interval '1 minute';")
44575a91d3 2025-07-09 192: .fetch_all(&mut *self.0).await.stack()?;
0340541002 2025-04-24 193: Ok(block)
0340541002 2025-04-24 194: }
0340541002 2025-04-24 195:
9adc69d514 2026-03-25 196: pub async fn get_list <I>(&mut self, owner: I) -> Result<Vec<List>>
bb89b6fab8 2025-06-15 197: where I: Into<i64> {
bb89b6fab8 2025-06-15 198: let source: Vec<List> = sqlx::query_as("select source_id, channel, enabled, url, iv_hash, url_re from rsstg_source where owner = $1 order by source_id")
bb89b6fab8 2025-06-15 199: .bind(owner.into())
44575a91d3 2025-07-09 200: .fetch_all(&mut *self.0).await.stack()?;
b4af85e31b 2025-06-29 201: Ok(source)
b4af85e31b 2025-06-29 202: }
b4af85e31b 2025-06-29 203:
b4af85e31b 2025-06-29 204: pub async fn get_one <I> (&mut self, owner: I, id: i32) -> Result<Option<List>>
b4af85e31b 2025-06-29 205: where I: Into<i64> {
b4af85e31b 2025-06-29 206: let source: Option<List> = sqlx::query_as("select source_id, channel, enabled, url, iv_hash, url_re from rsstg_source where owner = $1 and source_id = $2")
b4af85e31b 2025-06-29 207: .bind(owner.into())
b4af85e31b 2025-06-29 208: .bind(id)
9adc69d514 2026-03-25 209: .fetch_optional(&mut *self.0).await.stack()?;
9adc69d514 2026-03-25 210: Ok(source)
9adc69d514 2026-03-25 211: }
9adc69d514 2026-03-25 212:
9adc69d514 2026-03-25 213: pub async fn get_one_name <I> (&mut self, owner: I, name: &str) -> Result<Option<List>>
9adc69d514 2026-03-25 214: where I: Into<i64> {
9adc69d514 2026-03-25 215: let source: Option<List> = sqlx::query_as("select source_id, channel, enabled, url, iv_hash, url_re from rsstg_source where owner = $1 and channel = $2")
9adc69d514 2026-03-25 216: .bind(owner.into())
9adc69d514 2026-03-25 217: .bind(name)
44575a91d3 2025-07-09 218: .fetch_optional(&mut *self.0).await.stack()?;
bb89b6fab8 2025-06-15 219: Ok(source)
bb89b6fab8 2025-06-15 220: }
bb89b6fab8 2025-06-15 221:
bb89b6fab8 2025-06-15 222: pub async fn get_source <I> (&mut self, id: i32, owner: I) -> Result<Source>
bb89b6fab8 2025-06-15 223: where I: Into<i64> {
bb89b6fab8 2025-06-15 224: let source: Source = sqlx::query_as("select channel_id, url, iv_hash, owner, url_re from rsstg_source where source_id = $1 and owner = $2")
bb89b6fab8 2025-06-15 225: .bind(id)
bb89b6fab8 2025-06-15 226: .bind(owner.into())
44575a91d3 2025-07-09 227: .fetch_one(&mut *self.0).await.stack()?;
bb89b6fab8 2025-06-15 228: Ok(source)
bb89b6fab8 2025-06-15 229: }
bb89b6fab8 2025-06-15 230:
bb89b6fab8 2025-06-15 231: pub async fn set_scrape <I> (&mut self, id: I) -> Result<()>
bb89b6fab8 2025-06-15 232: where I: Into<i64> {
bb89b6fab8 2025-06-15 233: sqlx::query("update rsstg_source set last_scrape = now() where source_id = $1;")
bb89b6fab8 2025-06-15 234: .bind(id.into())
44575a91d3 2025-07-09 235: .execute(&mut *self.0).await.stack()?;
bb89b6fab8 2025-06-15 236: Ok(())
bb89b6fab8 2025-06-15 237: }
bb89b6fab8 2025-06-15 238:
d8c1d259a2 2026-04-23 239: #[allow(clippy::too_many_arguments)] // XXX do I need to make it variadic? I guess it work fine
d8c1d259a2 2026-04-23 240: // this way for now, unless there would be a good struct
d8c1d259a2 2026-04-23 241: // with all source fields to use that as an argument
bb89b6fab8 2025-06-15 242: pub async fn update <I> (&mut self, update: Option<i32>, channel: &str, channel_id: i64, url: &str, iv_hash: Option<&str>, url_re: Option<&str>, owner: I) -> Result<&str>
bb89b6fab8 2025-06-15 243: where I: Into<i64> {
0340541002 2025-04-24 244: match match update {
0340541002 2025-04-24 245: Some(id) => {
0340541002 2025-04-24 246: sqlx::query("update rsstg_source set channel_id = $2, url = $3, iv_hash = $4, owner = $5, channel = $6, url_re = $7 where source_id = $1")
0340541002 2025-04-24 247: .bind(id)
0340541002 2025-04-24 248: },
0340541002 2025-04-24 249: None => {
0340541002 2025-04-24 250: sqlx::query("insert into rsstg_source (channel_id, url, iv_hash, owner, channel, url_re) values ($1, $2, $3, $4, $5, $6)")
0340541002 2025-04-24 251: },
0340541002 2025-04-24 252: }
0340541002 2025-04-24 253: .bind(channel_id)
0340541002 2025-04-24 254: .bind(url)
0340541002 2025-04-24 255: .bind(iv_hash)
bb89b6fab8 2025-06-15 256: .bind(owner.into())
0340541002 2025-04-24 257: .bind(channel)
0340541002 2025-04-24 258: .bind(url_re)
c6d3e97290 2025-07-01 259: .execute(&mut *self.0).await
9adc69d514 2026-03-25 260: {
0340541002 2025-04-24 261: Ok(_) => Ok(match update {
0340541002 2025-04-24 262: Some(_) => "Channel updated.",
0340541002 2025-04-24 263: None => "Channel added.",
0340541002 2025-04-24 264: }),
0340541002 2025-04-24 265: Err(sqlx::Error::Database(err)) => {
0340541002 2025-04-24 266: match err.downcast::<sqlx::postgres::PgDatabaseError>().routine() {
0340541002 2025-04-24 267: Some("_bt_check_unique", ) => {
0340541002 2025-04-24 268: Ok("Duplicate key.")
0340541002 2025-04-24 269: },
0340541002 2025-04-24 270: Some(_) => {
0340541002 2025-04-24 271: Ok("Database error.")
0340541002 2025-04-24 272: },
0340541002 2025-04-24 273: None => {
0340541002 2025-04-24 274: Ok("No database error extracted.")
0340541002 2025-04-24 275: },
0340541002 2025-04-24 276: }
0340541002 2025-04-24 277: },
0340541002 2025-04-24 278: Err(err) => {
0340541002 2025-04-24 279: bail!("Sorry, unknown error:\n{err:#?}\n");
0340541002 2025-04-24 280: },
0340541002 2025-04-24 281: }
0340541002 2025-04-24 282: }
0340541002 2025-04-24 283: }