Annotation For src/sql.rs
Logged in as anonymous

Origin for each line in src/sql.rs from check-in 3fd8c40aa8:

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: 
374eadef45 2026-03-28  239: 	#[allow(clippy::too_many_arguments)] // XXX at least for now…
bb89b6fab8 2025-06-15  240: 	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  241: 	where I: Into<i64> {
0340541002 2025-04-24  242: 		match match update {
0340541002 2025-04-24  243: 				Some(id) => {
0340541002 2025-04-24  244: 					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  245: 						.bind(id)
0340541002 2025-04-24  246: 				},
0340541002 2025-04-24  247: 				None => {
0340541002 2025-04-24  248: 					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  249: 				},
0340541002 2025-04-24  250: 			}
0340541002 2025-04-24  251: 				.bind(channel_id)
0340541002 2025-04-24  252: 				.bind(url)
0340541002 2025-04-24  253: 				.bind(iv_hash)
bb89b6fab8 2025-06-15  254: 				.bind(owner.into())
0340541002 2025-04-24  255: 				.bind(channel)
0340541002 2025-04-24  256: 				.bind(url_re)
c6d3e97290 2025-07-01  257: 				.execute(&mut *self.0).await
9adc69d514 2026-03-25  258: 		{
0340541002 2025-04-24  259: 			Ok(_) => Ok(match update {
0340541002 2025-04-24  260: 				Some(_) => "Channel updated.",
0340541002 2025-04-24  261: 				None => "Channel added.",
0340541002 2025-04-24  262: 			}),
0340541002 2025-04-24  263: 			Err(sqlx::Error::Database(err)) => {
0340541002 2025-04-24  264: 				match err.downcast::<sqlx::postgres::PgDatabaseError>().routine() {
0340541002 2025-04-24  265: 					Some("_bt_check_unique", ) => {
0340541002 2025-04-24  266: 						Ok("Duplicate key.")
0340541002 2025-04-24  267: 					},
0340541002 2025-04-24  268: 					Some(_) => {
0340541002 2025-04-24  269: 						Ok("Database error.")
0340541002 2025-04-24  270: 					},
0340541002 2025-04-24  271: 					None => {
0340541002 2025-04-24  272: 						Ok("No database error extracted.")
0340541002 2025-04-24  273: 					},
0340541002 2025-04-24  274: 				}
0340541002 2025-04-24  275: 			},
0340541002 2025-04-24  276: 			Err(err) => {
0340541002 2025-04-24  277: 				bail!("Sorry, unknown error:\n{err:#?}\n");
0340541002 2025-04-24  278: 			},
0340541002 2025-04-24  279: 		}
0340541002 2025-04-24  280: 	}
0340541002 2025-04-24  281: }