From 95ab6c53856291a4d9cebb9fa5c5045031836120 Mon Sep 17 00:00:00 2001 From: Matthew Esposito Date: Wed, 20 Nov 2024 18:50:06 -0500 Subject: [PATCH 01/57] fix(oauth): update oauth resources and script --- scripts/update_oauth_resources.sh | 10 ++--- src/oauth_resources.rs | 70 ++++++++++++++++--------------- 2 files changed, 42 insertions(+), 38 deletions(-) diff --git a/scripts/update_oauth_resources.sh b/scripts/update_oauth_resources.sh index 1d6b486..a3014ae 100755 --- a/scripts/update_oauth_resources.sh +++ b/scripts/update_oauth_resources.sh @@ -39,12 +39,12 @@ done echo "];" >> "$filename" # Fetch Android app versions -page_1=$(curl -s "https://apkcombo.com/reddit/com.reddit.frontpage/old-versions/" | rg "" -r "https://apkcombo.com\$1" | sort | uniq) +page_1=$(curl -s "https://apkcombo.com/reddit/com.reddit.frontpage/old-versions/" | rg "" -r "https://apkcombo.com\$1" | sort | uniq | sed 's/ //g') # Append with pages -page_2=$(curl -s "https://apkcombo.com/reddit/com.reddit.frontpage/old-versions?page=2" | rg "" -r "https://apkcombo.com\$1" | sort | uniq) -page_3=$(curl -s "https://apkcombo.com/reddit/com.reddit.frontpage/old-versions?page=3" | rg "" -r "https://apkcombo.com\$1" | sort | uniq) -page_4=$(curl -s "https://apkcombo.com/reddit/com.reddit.frontpage/old-versions?page=4" | rg "" -r "https://apkcombo.com\$1" | sort | uniq) -page_5=$(curl -s "https://apkcombo.com/reddit/com.reddit.frontpage/old-versions?page=5" | rg "" -r "https://apkcombo.com\$1" | sort | uniq) +page_2=$(curl -s "https://apkcombo.com/reddit/com.reddit.frontpage/old-versions?page=2" | rg "" -r "https://apkcombo.com\$1" | sort | uniq | sed 's/ //g') +page_3=$(curl -s "https://apkcombo.com/reddit/com.reddit.frontpage/old-versions?page=3" | rg "" -r "https://apkcombo.com\$1" | sort | uniq | sed 's/ //g') +page_4=$(curl -s "https://apkcombo.com/reddit/com.reddit.frontpage/old-versions?page=4" | rg "" -r "https://apkcombo.com\$1" | sort | uniq | sed 's/ //g') +page_5=$(curl -s "https://apkcombo.com/reddit/com.reddit.frontpage/old-versions?page=5" | rg "" -r "https://apkcombo.com\$1" | sort | uniq | sed 's/ //g') # Concatenate all pages versions="${page_1}" diff --git a/src/oauth_resources.rs b/src/oauth_resources.rs index 3272939..a5dc2f3 100644 --- a/src/oauth_resources.rs +++ b/src/oauth_resources.rs @@ -2,8 +2,40 @@ // Rerun scripts/update_oauth_resources.sh to update this file // Please do not edit manually // Filled in with real app versions -pub static _IOS_APP_VERSION_LIST: &[&str; 1] = &[""]; +pub static _IOS_APP_VERSION_LIST: &[&str; 1] = &[ + "", +]; pub static ANDROID_APP_VERSION_LIST: &[&str; 150] = &[ + "Version 2024.22.1/Build 1652272", + "Version 2024.23.1/Build 1665606", + "Version 2024.24.1/Build 1682520", + "Version 2024.25.0/Build 1693595", + "Version 2024.25.2/Build 1700401", + "Version 2024.25.3/Build 1703490", + "Version 2024.26.0/Build 1710470", + "Version 2024.26.1/Build 1717435", + "Version 2024.28.0/Build 1737665", + "Version 2024.28.1/Build 1741165", + "Version 2024.30.0/Build 1770787", + "Version 2024.31.0/Build 1786202", + "Version 2024.32.0/Build 1809095", + "Version 2024.32.1/Build 1813258", + "Version 2024.33.0/Build 1819908", + "Version 2024.34.0/Build 1837909", + "Version 2024.35.0/Build 1861437", + "Version 2024.36.0/Build 1875012", + "Version 2024.37.0/Build 1888053", + "Version 2024.38.0/Build 1902791", + "Version 2024.39.0/Build 1916713", + "Version 2024.40.0/Build 1928580", + "Version 2024.41.0/Build 1941199", + "Version 2024.41.1/Build 1947805", + "Version 2024.42.0/Build 1952440", + "Version 2024.43.0/Build 1972250", + "Version 2024.44.0/Build 1988458", + "Version 2024.45.0/Build 2001943", + "Version 2024.46.0/Build 2012731", + "Version 2024.47.0/Build 2029755", "Version 2023.48.0/Build 1319123", "Version 2023.49.0/Build 1321715", "Version 2023.49.1/Build 1322281", @@ -31,9 +63,9 @@ pub static ANDROID_APP_VERSION_LIST: &[&str; 150] = &[ "Version 2024.20.0/Build 1612800", "Version 2024.20.1/Build 1615586", "Version 2024.20.2/Build 1624969", + "Version 2024.20.3/Build 1624970", "Version 2024.21.0/Build 1631686", "Version 2024.22.0/Build 1645257", - "Version 2024.22.1/Build 1652272", "Version 2023.21.0/Build 956283", "Version 2023.22.0/Build 968223", "Version 2023.23.0/Build 983896", @@ -124,35 +156,7 @@ pub static ANDROID_APP_VERSION_LIST: &[&str; 150] = &[ "Version 2022.40.0/Build 624782", "Version 2022.41.0/Build 630468", "Version 2022.41.1/Build 634168", - "Version 2021.39.1/Build 372418", - "Version 2021.41.0/Build 376052", - "Version 2021.42.0/Build 378193", - "Version 2021.43.0/Build 382019", - "Version 2021.44.0/Build 385129", - "Version 2021.45.0/Build 387663", - "Version 2021.46.0/Build 392043", - "Version 2021.47.0/Build 394342", - "Version 2022.10.0/Build 429896", - "Version 2022.1.0/Build 402829", - "Version 2022.11.0/Build 433004", - "Version 2022.12.0/Build 436848", - "Version 2022.13.0/Build 442084", - "Version 2022.13.1/Build 444621", - "Version 2022.14.1/Build 452742", - "Version 2022.15.0/Build 455453", - "Version 2022.16.0/Build 462377", - "Version 2022.17.0/Build 468480", - "Version 2022.18.0/Build 473740", - "Version 2022.19.1/Build 482464", - "Version 2022.2.0/Build 405543", - "Version 2022.3.0/Build 408637", - "Version 2022.4.0/Build 411368", - "Version 2022.5.0/Build 414731", - "Version 2022.6.0/Build 418391", - "Version 2022.6.1/Build 419585", - "Version 2022.6.2/Build 420562", - "Version 2022.7.0/Build 420849", - "Version 2022.8.0/Build 423906", - "Version 2022.9.0/Build 426592", ]; -pub static _IOS_OS_VERSION_LIST: &[&str; 1] = &[""]; +pub static _IOS_OS_VERSION_LIST: &[&str; 1] = &[ + "", +]; From 6be6f892a4eb159f5a27ce48f0ce615298071eac Mon Sep 17 00:00:00 2001 From: Matthew Esposito Date: Wed, 20 Nov 2024 19:19:29 -0500 Subject: [PATCH 02/57] feat(oauth): better oauth client matching --- Cargo.lock | 15 +++++++++++++++ Cargo.toml | 1 + src/client.rs | 36 +++++++++++++----------------------- src/oauth.rs | 20 +++++++++++++++----- src/oauth_resources.rs | 8 ++------ 5 files changed, 46 insertions(+), 34 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4143b80..057234f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1327,6 +1327,8 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ + "libc", + "rand_chacha", "rand_core", ] @@ -1345,6 +1347,9 @@ name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] [[package]] name = "redlib" @@ -1380,6 +1385,7 @@ dependencies = [ "serde_json", "serde_json_path", "serde_yaml", + "tegen", "time", "tokio", "toml", @@ -1895,6 +1901,15 @@ dependencies = [ "syn", ] +[[package]] +name = "tegen" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a2d5a357b7c859b410139734a875136473c3b18b1bbd8d5bdc1769d9002acd" +dependencies = [ + "rand", +] + [[package]] name = "tempfile" version = "3.14.0" diff --git a/Cargo.toml b/Cargo.toml index 7bb7e93..616d8e9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,6 +49,7 @@ serde_json_path = "0.7.1" async-recursion = "1.1.1" common-words-all = { version = "0.0.2", default-features = false, features = ["english", "one"] } hyper-rustls = { version = "0.24.2", features = [ "http2" ] } +tegen = "0.1.4" [dev-dependencies] diff --git a/src/client.rs b/src/client.rs index 248fc88..1e1661d 100644 --- a/src/client.rs +++ b/src/client.rs @@ -218,40 +218,30 @@ fn request(method: &'static Method, path: String, redirect: bool, quarantine: bo // Construct the hyper client from the HTTPS connector. let client: &Lazy> = &CLIENT; - let (token, vendor_id, device_id, user_agent, loid) = { - let client = OAUTH_CLIENT.load_full(); - ( - client.token.clone(), - client.headers_map.get("Client-Vendor-Id").cloned().unwrap_or_default(), - client.headers_map.get("X-Reddit-Device-Id").cloned().unwrap_or_default(), - client.headers_map.get("User-Agent").cloned().unwrap_or_default(), - client.headers_map.get("x-reddit-loid").cloned().unwrap_or_default(), - ) - }; - // Build request to Reddit. When making a GET, request gzip compression. // (Reddit doesn't do brotli yet.) - let mut headers = vec![ - ("User-Agent", user_agent), - ("Client-Vendor-Id", vendor_id), - ("X-Reddit-Device-Id", device_id), - ("x-reddit-loid", loid), - ("Host", host.to_string()), - ("Authorization", format!("Bearer {token}")), - ("Accept-Encoding", if method == Method::GET { "gzip".into() } else { "identity".into() }), + let mut headers: Vec<(String, String)> = vec![ + ("Host".into(), host.into()), + ("Accept-Encoding".into(), if method == Method::GET { "gzip".into() } else { "identity".into() }), ( - "Cookie", + "Cookie".into(), if quarantine { "_options=%7B%22pref_quarantine_optin%22%3A%20true%2C%20%22pref_gated_sr_optin%22%3A%20true%7D".into() } else { "".into() }, ), - ("X-Reddit-Width", fastrand::u32(300..500).to_string()), - ("X-Reddit-DPR", "2".to_owned()), - ("Device-Name", format!("Android {}", fastrand::u8(9..=14))), ]; + { + let client = OAUTH_CLIENT.load_full(); + for (key, value) in client.initial_headers.clone() { + headers.push((key, value)); + } + } + + trace!("Headers: {:#?}", headers); + // shuffle headers: https://github.com/redlib-org/redlib/issues/324 fastrand::shuffle(&mut headers); diff --git a/src/oauth.rs b/src/oauth.rs index 80bf318..576b647 100644 --- a/src/oauth.rs +++ b/src/oauth.rs @@ -7,8 +7,8 @@ use crate::{ use base64::{engine::general_purpose, Engine as _}; use hyper::{client, Body, Method, Request}; use log::{error, info, trace}; - use serde_json::json; +use tegen::tegen::TextGenerator; use tokio::time::{error::Elapsed, timeout}; static REDDIT_ANDROID_OAUTH_CLIENT_ID: &str = "ohXpoqrZYub1kg"; @@ -84,7 +84,7 @@ impl Oauth { // Set JSON body. I couldn't tell you what this means. But that's what the client sends let json = json!({ - "scopes": ["*","email"] + "scopes": ["*","email", "pii"] }); let body = Body::from(json.to_string()); @@ -185,11 +185,21 @@ impl Device { let android_user_agent = format!("Reddit/{android_app_version}/Android {android_version}"); + let qos = fastrand::u32(1000..=100_000); + let qos: f32 = qos as f32 / 1000.0; + let qos = format!("{:.3}", qos); + + let codecs = TextGenerator::new().generate("available-codecs=video/avc, video/hevc{, video/x-vnd.on2.vp9|}"); + // Android device headers - let headers = HashMap::from([ - ("Client-Vendor-Id".into(), uuid.clone()), - ("X-Reddit-Device-Id".into(), uuid.clone()), + let headers: HashMap = HashMap::from([ ("User-Agent".into(), android_user_agent), + ("x-reddit-retry".into(), "algo=no-retries".into()), + ("x-reddit-compression".into(), "1".into()), + ("x-reddit-qos".into(), qos), + ("x-reddit-media-codecs".into(), codecs), + ("Content-Type".into(), "application/json; charset=UTF-8".into()), + ("client-vendor-id".into(), uuid.clone()), ]); info!("[🔄] Spoofing Android client with headers: {headers:?}, uuid: \"{uuid}\", and OAuth ID \"{REDDIT_ANDROID_OAUTH_CLIENT_ID}\""); diff --git a/src/oauth_resources.rs b/src/oauth_resources.rs index a5dc2f3..faf7873 100644 --- a/src/oauth_resources.rs +++ b/src/oauth_resources.rs @@ -2,9 +2,7 @@ // Rerun scripts/update_oauth_resources.sh to update this file // Please do not edit manually // Filled in with real app versions -pub static _IOS_APP_VERSION_LIST: &[&str; 1] = &[ - "", -]; +pub static _IOS_APP_VERSION_LIST: &[&str; 1] = &[""]; pub static ANDROID_APP_VERSION_LIST: &[&str; 150] = &[ "Version 2024.22.1/Build 1652272", "Version 2024.23.1/Build 1665606", @@ -157,6 +155,4 @@ pub static ANDROID_APP_VERSION_LIST: &[&str; 150] = &[ "Version 2022.41.0/Build 630468", "Version 2022.41.1/Build 634168", ]; -pub static _IOS_OS_VERSION_LIST: &[&str; 1] = &[ - "", -]; +pub static _IOS_OS_VERSION_LIST: &[&str; 1] = &[""]; From 100a7b65a6a79968cbc8548f4ce12d722dfb0cba Mon Sep 17 00:00:00 2001 From: Matthew Esposito Date: Sat, 23 Nov 2024 21:17:52 -0500 Subject: [PATCH 03/57] fix(client): update headers management, add self check (fix #334, fix #318) --- src/client.rs | 41 ++++++++++++++++++++++++++++++++++++++--- src/main.rs | 12 +++++++++++- src/oauth.rs | 6 ++++-- 3 files changed, 53 insertions(+), 6 deletions(-) diff --git a/src/client.rs b/src/client.rs index 1e1661d..0e2c301 100644 --- a/src/client.rs +++ b/src/client.rs @@ -19,6 +19,7 @@ use std::{io, result::Result}; use crate::dbg_msg; use crate::oauth::{force_refresh_token, token_daemon, Oauth}; use crate::server::RequestExt; +use crate::subreddit::community; use crate::utils::format_url; const REDDIT_URL_BASE: &str = "https://oauth.reddit.com"; @@ -235,13 +236,11 @@ fn request(method: &'static Method, path: String, redirect: bool, quarantine: bo { let client = OAUTH_CLIENT.load_full(); - for (key, value) in client.initial_headers.clone() { + for (key, value) in client.headers_map.clone() { headers.push((key, value)); } } - trace!("Headers: {:#?}", headers); - // shuffle headers: https://github.com/redlib-org/redlib/issues/324 fastrand::shuffle(&mut headers); @@ -390,6 +389,12 @@ pub async fn json(path: String, quarantine: bool) -> Result { "Ratelimit remaining: Header says {remaining}, we have {current_rate_limit}. Resets in {reset}. Rollover: {}. Ratelimit used: {used}", if is_rolling_over { "yes" } else { "no" }, ); + + // If can parse remaining as a float, round to a u16 and save + if let Ok(val) = remaining.parse::() { + OAUTH_RATELIMIT_REMAINING.store(val.round() as u16, Ordering::SeqCst); + } + Some(reset) } else { None @@ -474,6 +479,36 @@ pub async fn json(path: String, quarantine: bool) -> Result { } } +async fn self_check(sub: &str) -> Result<(), String> { + let request = Request::get(format!("/r/{sub}/")).body(Body::empty()).unwrap(); + + match community(request).await { + Ok(sub) if sub.status().is_success() => Ok(()), + Ok(sub) => Err(sub.status().to_string()), + Err(e) => Err(e), + } +} + +pub async fn rate_limit_check() -> Result<(), String> { + // First, check a subreddit. + self_check("reddit").await?; + // This will reduce the rate limit to 99. Assert this check. + if OAUTH_RATELIMIT_REMAINING.load(Ordering::SeqCst) != 99 { + return Err(format!("Rate limit check failed: expected 99, got {}", OAUTH_RATELIMIT_REMAINING.load(Ordering::SeqCst))); + } + // Now, we switch out the OAuth client. + // This checks for the IP rate limit association. + force_refresh_token().await; + // Now, check a new sub to break cache. + self_check("rust").await?; + // Again, assert the rate limit check. + if OAUTH_RATELIMIT_REMAINING.load(Ordering::SeqCst) != 99 { + return Err(format!("Rate limit check failed: expected 99, got {}", OAUTH_RATELIMIT_REMAINING.load(Ordering::SeqCst))); + } + + Ok(()) +} + #[cfg(test)] static POPULAR_URL: &str = "/r/popular/hot.json?&raw_json=1&geo_filter=GLOBAL"; diff --git a/src/main.rs b/src/main.rs index 7342597..abae968 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,7 +11,7 @@ use hyper::Uri; use hyper::{header::HeaderValue, Body, Request, Response}; use log::info; use once_cell::sync::Lazy; -use redlib::client::{canonical_path, proxy, CLIENT}; +use redlib::client::{canonical_path, proxy, rate_limit_check, CLIENT}; use redlib::server::{self, RequestExt}; use redlib::utils::{error, redirect, ThemeAssets}; use redlib::{config, duplicates, headers, instance_info, post, search, settings, subreddit, user}; @@ -146,6 +146,16 @@ async fn main() { ) .get_matches(); + match rate_limit_check().await { + Ok(()) => { + info!("[✅] Rate limit check passed"); + }, + Err(e) => { + log::error!("[❌] Rate limit check failed: {}", e); + std::process::exit(1); + } + } + let address = matches.get_one::("address").unwrap(); let port = matches.get_one::("port").unwrap(); let hsts = matches.get_one("hsts").map(|m: &String| m.as_str()); diff --git a/src/oauth.rs b/src/oauth.rs index 576b647..12b0f37 100644 --- a/src/oauth.rs +++ b/src/oauth.rs @@ -38,12 +38,12 @@ impl Oauth { } Ok(None) => { error!("Failed to create OAuth client. Retrying in 5 seconds..."); - continue; } Err(duration) => { error!("Failed to create OAuth client in {duration:?}. Retrying in 5 seconds..."); } } + tokio::time::sleep(Duration::from_secs(5)).await; } } @@ -91,13 +91,14 @@ impl Oauth { // Build request let request = builder.body(body).unwrap(); - trace!("Sending token request..."); + trace!("Sending token request...\n\n{request:?}"); // Send request let client: &once_cell::sync::Lazy> = &CLIENT; let resp = client.request(request).await.ok()?; trace!("Received response with status {} and length {:?}", resp.status(), resp.headers().get("content-length")); + trace!("OAuth headers: {:#?}", resp.headers()); // Parse headers - loid header _should_ be saved sent on subsequent token refreshes. // Technically it's not needed, but it's easy for Reddit API to check for this. @@ -200,6 +201,7 @@ impl Device { ("x-reddit-media-codecs".into(), codecs), ("Content-Type".into(), "application/json; charset=UTF-8".into()), ("client-vendor-id".into(), uuid.clone()), + ("X-Reddit-Device-Id".into(), uuid.clone()), ]); info!("[🔄] Spoofing Android client with headers: {headers:?}, uuid: \"{uuid}\", and OAuth ID \"{REDDIT_ANDROID_OAUTH_CLIENT_ID}\""); From 7fe109df2267f292459c2154554c7bb40e77908d Mon Sep 17 00:00:00 2001 From: Matthew Esposito Date: Sat, 23 Nov 2024 21:41:30 -0500 Subject: [PATCH 04/57] style(clippy) --- src/main.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main.rs b/src/main.rs index abae968..9c8de97 100644 --- a/src/main.rs +++ b/src/main.rs @@ -149,9 +149,10 @@ async fn main() { match rate_limit_check().await { Ok(()) => { info!("[✅] Rate limit check passed"); - }, + } Err(e) => { - log::error!("[❌] Rate limit check failed: {}", e); + log::error!(" Rate limit check failed: {}", e); + println!("[❌] Rate limit check failed: {}", e); std::process::exit(1); } } From a4f511f67e350fb4e4792416d42dbeaa9f1d544b Mon Sep 17 00:00:00 2001 From: Matthew Esposito Date: Sun, 24 Nov 2024 10:50:21 -0500 Subject: [PATCH 05/57] fix(client): update rate limit self-check (fix #335) --- src/client.rs | 30 ++++++++++++++++++++++++------ src/subreddit.rs | 3 +++ 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/src/client.rs b/src/client.rs index 0e2c301..ba08531 100644 --- a/src/client.rs +++ b/src/client.rs @@ -19,8 +19,7 @@ use std::{io, result::Result}; use crate::dbg_msg; use crate::oauth::{force_refresh_token, token_daemon, Oauth}; use crate::server::RequestExt; -use crate::subreddit::community; -use crate::utils::format_url; +use crate::utils::{format_url, Post}; const REDDIT_URL_BASE: &str = "https://oauth.reddit.com"; const REDDIT_URL_BASE_HOST: &str = "oauth.reddit.com"; @@ -480,11 +479,10 @@ pub async fn json(path: String, quarantine: bool) -> Result { } async fn self_check(sub: &str) -> Result<(), String> { - let request = Request::get(format!("/r/{sub}/")).body(Body::empty()).unwrap(); + let query = format!("/r/{sub}/hot.json?&raw_json=1"); - match community(request).await { - Ok(sub) if sub.status().is_success() => Ok(()), - Ok(sub) => Err(sub.status().to_string()), + match Post::fetch(&query, true).await { + Ok(_) => Ok(()), Err(e) => Err(e), } } @@ -509,6 +507,26 @@ pub async fn rate_limit_check() -> Result<(), String> { Ok(()) } +#[cfg(test)] +use {crate::config::get_setting, sealed_test::prelude::*}; + +#[tokio::test(flavor = "multi_thread")] +async fn test_rate_limit_check() { + rate_limit_check().await.unwrap(); +} + +#[test] +#[sealed_test(env = [("REDLIB_DEFAULT_SUBSCRIPTIONS", "rust")])] +fn test_default_subscriptions() { + tokio::runtime::Builder::new_multi_thread().enable_all().build().unwrap().block_on(async { + let subscriptions = get_setting("REDLIB_DEFAULT_SUBSCRIPTIONS"); + assert!(subscriptions.is_some()); + + // check rate limit + rate_limit_check().await.unwrap(); + }); +} + #[cfg(test)] static POPULAR_URL: &str = "/r/popular/hot.json?&raw_json=1&geo_filter=GLOBAL"; diff --git a/src/subreddit.rs b/src/subreddit.rs index 3a07bdc..88aa542 100644 --- a/src/subreddit.rs +++ b/src/subreddit.rs @@ -8,6 +8,7 @@ use crate::utils::{ use crate::{client::json, server::RequestExt, server::ResponseExt}; use cookie::Cookie; use hyper::{Body, Request, Response}; +use log::{debug, trace}; use rinja::Template; use once_cell::sync::Lazy; @@ -62,6 +63,7 @@ pub async fn community(req: Request) -> Result, String> { // Build Reddit API path let root = req.uri().path() == "/"; let query = req.uri().query().unwrap_or_default().to_string(); + trace!("query: {}", query); let subscribed = setting(&req, "subscriptions"); let front_page = setting(&req, "front_page"); let post_sort = req.cookie("post_sort").map_or_else(|| "hot".to_string(), |c| c.value().to_string()); @@ -123,6 +125,7 @@ pub async fn community(req: Request) -> Result, String> { } let path = format!("/r/{}/{sort}.json?{}{params}", sub_name.replace('+', "%2B"), req.uri().query().unwrap_or_default()); + debug!("Path: {}", path); let url = String::from(req.uri().path_and_query().map_or("", |val| val.as_str())); let redirect_url = url[1..].replace('?', "%3F").replace('&', "%26").replace('+', "%2B"); let filters = get_filters(&req); From 9f6b08cbb2d0f43644a34f5d0210ac32b9add30c Mon Sep 17 00:00:00 2001 From: Matthew Esposito Date: Tue, 26 Nov 2024 22:55:48 -0500 Subject: [PATCH 06/57] fix(main): reduce rate limit check fail to warned error --- src/main.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/main.rs b/src/main.rs index 9c8de97..8732d20 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,7 +9,7 @@ use std::str::FromStr; use futures_lite::FutureExt; use hyper::Uri; use hyper::{header::HeaderValue, Body, Request, Response}; -use log::info; +use log::{info, warn}; use once_cell::sync::Lazy; use redlib::client::{canonical_path, proxy, rate_limit_check, CLIENT}; use redlib::server::{self, RequestExt}; @@ -151,9 +151,12 @@ async fn main() { info!("[✅] Rate limit check passed"); } Err(e) => { - log::error!(" Rate limit check failed: {}", e); - println!("[❌] Rate limit check failed: {}", e); - std::process::exit(1); + let mut message = format!("Rate limit check failed: {}", e); + message += "\nThis may cause issues with the rate limit."; + message += "\nPlease report this error with the above information."; + message += "\nhttps://github.com/redlib-org/redlib/issues/new?assignees=sigaloid&labels=bug&title=%F0%9F%90%9B+Bug+Report%3A+Rate+limit+mismatch"; + warn!("{}", message); + eprintln!("{}", message); } } From e4fc22cf906b7e8213e0b96108106a9ad34c0e29 Mon Sep 17 00:00:00 2001 From: Integral Date: Tue, 3 Dec 2024 00:28:31 +0800 Subject: [PATCH 07/57] refactor: replace static with const for global constants (#340) --- scripts/update_oauth_resources.sh | 6 +++--- src/client.rs | 6 +++--- src/oauth.rs | 4 ++-- src/oauth_resources.rs | 6 +++--- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/scripts/update_oauth_resources.sh b/scripts/update_oauth_resources.sh index a3014ae..7eeb959 100755 --- a/scripts/update_oauth_resources.sh +++ b/scripts/update_oauth_resources.sh @@ -24,7 +24,7 @@ echo "// Please do not edit manually" >> "$filename" echo "// Filled in with real app versions" >> "$filename" # Open the array in the source file -echo "pub static _IOS_APP_VERSION_LIST: &[&str; $ios_app_count] = &[" >> "$filename" +echo "pub const _IOS_APP_VERSION_LIST: &[&str; $ios_app_count] = &[" >> "$filename" num=0 @@ -63,7 +63,7 @@ android_count=$(echo "$versions" | wc -l) echo -e "Fetching \e[32m$android_count Android app versions...\e[0m" # Append to the source file -echo "pub static ANDROID_APP_VERSION_LIST: &[&str; $android_count] = &[" >> "$filename" +echo "pub const ANDROID_APP_VERSION_LIST: &[&str; $android_count] = &[" >> "$filename" num=0 @@ -89,7 +89,7 @@ ios_count=$(echo "$table" | wc -l) echo -e "Fetching \e[34m$ios_count iOS versions...\e[0m" # Append to the source file -echo "pub static _IOS_OS_VERSION_LIST: &[&str; $ios_count] = &[" >> "$filename" +echo "pub const _IOS_OS_VERSION_LIST: &[&str; $ios_count] = &[" >> "$filename" num=0 diff --git a/src/client.rs b/src/client.rs index ba08531..fa32fc0 100644 --- a/src/client.rs +++ b/src/client.rs @@ -45,7 +45,7 @@ pub static OAUTH_RATELIMIT_REMAINING: AtomicU16 = AtomicU16::new(99); pub static OAUTH_IS_ROLLING_OVER: AtomicBool = AtomicBool::new(false); -static URL_PAIRS: [(&str, &str); 2] = [ +const URL_PAIRS: [(&str, &str); 2] = [ (ALTERNATIVE_REDDIT_URL_BASE, ALTERNATIVE_REDDIT_URL_BASE_HOST), (REDDIT_SHORT_URL_BASE, REDDIT_SHORT_URL_BASE_HOST), ]; @@ -262,7 +262,7 @@ fn request(method: &'static Method, path: String, redirect: bool, quarantine: bo return Ok(response); }; let location_header = response.headers().get(header::LOCATION); - if location_header == Some(&HeaderValue::from_static("https://www.reddit.com/")) { + if location_header == Some(&HeaderValue::from_static(ALTERNATIVE_REDDIT_URL_BASE)) { return Err("Reddit response was invalid".to_string()); } return request( @@ -528,7 +528,7 @@ fn test_default_subscriptions() { } #[cfg(test)] -static POPULAR_URL: &str = "/r/popular/hot.json?&raw_json=1&geo_filter=GLOBAL"; +const POPULAR_URL: &str = "/r/popular/hot.json?&raw_json=1&geo_filter=GLOBAL"; #[tokio::test(flavor = "multi_thread")] async fn test_localization_popular() { diff --git a/src/oauth.rs b/src/oauth.rs index 12b0f37..5627900 100644 --- a/src/oauth.rs +++ b/src/oauth.rs @@ -11,9 +11,9 @@ use serde_json::json; use tegen::tegen::TextGenerator; use tokio::time::{error::Elapsed, timeout}; -static REDDIT_ANDROID_OAUTH_CLIENT_ID: &str = "ohXpoqrZYub1kg"; +const REDDIT_ANDROID_OAUTH_CLIENT_ID: &str = "ohXpoqrZYub1kg"; -static AUTH_ENDPOINT: &str = "https://www.reddit.com"; +const AUTH_ENDPOINT: &str = "https://www.reddit.com"; // Spoofed client for Android devices #[derive(Debug, Clone, Default)] diff --git a/src/oauth_resources.rs b/src/oauth_resources.rs index faf7873..01928c3 100644 --- a/src/oauth_resources.rs +++ b/src/oauth_resources.rs @@ -2,8 +2,8 @@ // Rerun scripts/update_oauth_resources.sh to update this file // Please do not edit manually // Filled in with real app versions -pub static _IOS_APP_VERSION_LIST: &[&str; 1] = &[""]; -pub static ANDROID_APP_VERSION_LIST: &[&str; 150] = &[ +pub const _IOS_APP_VERSION_LIST: &[&str; 1] = &[""]; +pub const ANDROID_APP_VERSION_LIST: &[&str; 150] = &[ "Version 2024.22.1/Build 1652272", "Version 2024.23.1/Build 1665606", "Version 2024.24.1/Build 1682520", @@ -155,4 +155,4 @@ pub static ANDROID_APP_VERSION_LIST: &[&str; 150] = &[ "Version 2022.41.0/Build 630468", "Version 2022.41.1/Build 634168", ]; -pub static _IOS_OS_VERSION_LIST: &[&str; 1] = &[""]; +pub const _IOS_OS_VERSION_LIST: &[&str; 1] = &[""]; From d7ec07cd0d713fc308e1004663b0053db8f00a0f Mon Sep 17 00:00:00 2001 From: Jeidnx Date: Mon, 2 Dec 2024 17:29:57 +0100 Subject: [PATCH 08/57] Implement a serializer for user preferences (#336) --- Cargo.lock | 13 +++++++++++ Cargo.toml | 1 + src/utils.rs | 49 ++++++++++++++++++++++++++++++++++++++--- templates/settings.html | 12 ++++++++-- 4 files changed, 70 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 057234f..819d4bc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1384,6 +1384,7 @@ dependencies = [ "serde", "serde_json", "serde_json_path", + "serde_urlencoded", "serde_yaml", "tegen", "time", @@ -1797,6 +1798,18 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + [[package]] name = "serde_yaml" version = "0.9.34+deprecated" diff --git a/Cargo.toml b/Cargo.toml index 616d8e9..a1d3ec0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,6 +50,7 @@ async-recursion = "1.1.1" common-words-all = { version = "0.0.2", default-features = false, features = ["english", "one"] } hyper-rustls = { version = "0.24.2", features = [ "http2" ] } tegen = "0.1.4" +serde_urlencoded = "0.7.1" [dev-dependencies] diff --git a/src/utils.rs b/src/utils.rs index 1edb528..c15dcea 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -13,7 +13,7 @@ use once_cell::sync::Lazy; use regex::Regex; use rinja::Template; use rust_embed::RustEmbed; -use serde::Serialize; +use serde::{Serialize, Serializer}; use serde_json::Value; use serde_json_path::{JsonPath, JsonPathExt}; use std::collections::{HashMap, HashSet}; @@ -601,8 +601,9 @@ pub struct Params { pub before: Option, } -#[derive(Default)] +#[derive(Default, Serialize)] pub struct Preferences { + #[serde(skip)] pub available_themes: Vec, pub theme: String, pub front_page: String, @@ -620,12 +621,21 @@ pub struct Preferences { pub disable_visit_reddit_confirmation: String, pub comment_sort: String, pub post_sort: String, + #[serde(serialize_with = "serialize_vec_with_plus")] pub subscriptions: Vec, + #[serde(serialize_with = "serialize_vec_with_plus")] pub filters: Vec, pub hide_awards: String, pub hide_score: String, } +fn serialize_vec_with_plus(vec: &Vec, serializer: S) -> Result +where + S: Serializer, +{ + serializer.serialize_str(&vec.join("+")) +} + #[derive(RustEmbed)] #[folder = "static/themes/"] #[include = "*.css"] @@ -665,6 +675,10 @@ impl Preferences { hide_score: setting(req, "hide_score"), } } + + pub fn to_urlencoded(&self) -> Result { + serde_urlencoded::to_string(self).map_err(|e| e.to_string()) + } } /// Gets a `HashSet` of filters from the cookie in the given `Request`. @@ -1277,7 +1291,7 @@ pub fn get_post_url(post: &Post) -> String { #[cfg(test)] mod tests { - use super::{format_num, format_url, rewrite_urls}; + use super::{format_num, format_url, rewrite_urls, Preferences}; #[test] fn format_num_works() { @@ -1344,6 +1358,35 @@ mod tests { assert_eq!(format_url("nsfw"), ""); assert_eq!(format_url("spoiler"), ""); } + #[test] + fn serialize_prefs() { + let prefs = Preferences { + available_themes: vec![], + theme: "laserwave".to_owned(), + front_page: "default".to_owned(), + layout: "compact".to_owned(), + wide: "on".to_owned(), + blur_spoiler: "on".to_owned(), + show_nsfw: "off".to_owned(), + blur_nsfw: "on".to_owned(), + hide_hls_notification: "off".to_owned(), + video_quality: "best".to_owned(), + hide_sidebar_and_summary: "off".to_owned(), + use_hls: "on".to_owned(), + autoplay_videos: "on".to_owned(), + fixed_navbar: "on".to_owned(), + disable_visit_reddit_confirmation: "on".to_owned(), + comment_sort: "confidence".to_owned(), + post_sort: "top".to_owned(), + subscriptions: vec!["memes".to_owned(), "mildlyinteresting".to_owned()], + filters: vec![], + hide_awards: "off".to_owned(), + hide_score: "off".to_owned(), + }; + let urlencoded = serde_urlencoded::to_string(prefs).expect("Failed to serialize Prefs"); + + assert_eq!(urlencoded, "theme=laserwave&front_page=default&layout=compact&wide=on&blur_spoiler=on&show_nsfw=off&blur_nsfw=on&hide_hls_notification=off&video_quality=best&hide_sidebar_and_summary=off&use_hls=on&autoplay_videos=on&fixed_navbar=on&disable_visit_reddit_confirmation=on&comment_sort=confidence&post_sort=top&subscriptions=memes%2Bmildlyinteresting&filters=&hide_awards=off&hide_score=off") + } } #[test] diff --git a/templates/settings.html b/templates/settings.html index a7d6615..fef91cf 100644 --- a/templates/settings.html +++ b/templates/settings.html @@ -161,8 +161,16 @@ {% endif %} From bfcc946baad150b790590a436795b53197b9f7df Mon Sep 17 00:00:00 2001 From: ayaka Date: Sat, 11 Jan 2025 15:05:18 +1300 Subject: [PATCH 09/57] subreddit banners --- src/subreddit.rs | 5 +++++ src/utils.rs | 1 + static/style.css | 2 +- templates/subreddit.html | 8 +++++++- 4 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/subreddit.rs b/src/subreddit.rs index 5f964ef..8569185 100644 --- a/src/subreddit.rs +++ b/src/subreddit.rs @@ -469,6 +469,10 @@ async fn subreddit(sub: &str, quarantined: bool) -> Result { let community_icon: &str = res["data"]["community_icon"].as_str().unwrap_or_default(); let icon = if community_icon.is_empty() { val(&res, "icon_img") } else { community_icon.to_string() }; + // Fetch subreddit banner either from the banner_background_image or banner_img value + let banner_background_image: &str = res["data"]["banner_background_image"].as_str().unwrap_or_default(); + let banner = if banner_background_image.is_empty() { val(&res, "banner_img") } else { banner_background_image.to_string() }; + Ok(Subreddit { name: val(&res, "display_name"), title: val(&res, "title"), @@ -476,6 +480,7 @@ async fn subreddit(sub: &str, quarantined: bool) -> Result { info: rewrite_urls(&val(&res, "description_html")), // moderators: moderators_list(sub, quarantined).await.unwrap_or_default(), icon: format_url(&icon), + banner: format_url(&banner), members: format_num(members), active: format_num(active), wiki: res["data"]["wiki_enabled"].as_bool().unwrap_or_default(), diff --git a/src/utils.rs b/src/utils.rs index b8105c3..ba09800 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -585,6 +585,7 @@ pub struct Subreddit { pub info: String, // pub moderators: Vec, pub icon: String, + pub banner: String, pub members: (String, String), pub active: (String, String), pub wiki: bool, diff --git a/static/style.css b/static/style.css index 9567a81..df9f8da 100644 --- a/static/style.css +++ b/static/style.css @@ -499,7 +499,7 @@ aside { height: 100px; border: 2px solid var(--accent); border-radius: 100%; - padding: 10px; + padding: 0px; margin: 10px; } diff --git a/templates/subreddit.html b/templates/subreddit.html index 5584863..2765e1f 100644 --- a/templates/subreddit.html +++ b/templates/subreddit.html @@ -100,8 +100,14 @@ Wiki {% endif %} -
+ {% block head %} + {% call super() %} + + {% endblock %} +
Icon for r/{{ sub.name }} +
+

{{ sub.title }}

r/{{ sub.name }}

{% if crate::utils::enable_rss() %} From 200509255c0039c163f35b9332fbfb70c900be78 Mon Sep 17 00:00:00 2001 From: ayaka Date: Mon, 13 Jan 2025 14:15:35 +1300 Subject: [PATCH 10/57] allow disabling of banner --- README.md | 1 + src/config.rs | 6 ++++++ src/settings.rs | 3 ++- src/utils.rs | 2 ++ templates/settings.html | 7 ++++++- 5 files changed, 17 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 86764d3..b71973c 100644 --- a/README.md +++ b/README.md @@ -338,4 +338,5 @@ Assign a default value for each user-modifiable setting by passing environment v | `DISABLE_VISIT_REDDIT_CONFIRMATION` | `["on", "off"]` | `off` | | `HIDE_SCORE` | `["on", "off"]` | `off` | | `HIDE_SIDEBAR_AND_SUMMARY` | `["on", "off"]` | `off` | +| `HIDE_BANNER` | `["on", "off"]` | `off` | | `FIXED_NAVBAR` | `["on", "off"]` | `on` | diff --git a/src/config.rs b/src/config.rs index 1c1adbe..9b9fe75 100644 --- a/src/config.rs +++ b/src/config.rs @@ -84,6 +84,10 @@ pub struct Config { #[serde(alias = "LIBREDDIT_DEFAULT_HIDE_SIDEBAR_AND_SUMMARY")] pub(crate) default_hide_sidebar_and_summary: Option, + #[serde(rename = "REDLIB_DEFAULT_HIDE_BANNER")] + #[serde(alias = "LIBREDDIT_DEFAULT_HIDE_BANNER")] + pub(crate) default_hide_banner: Option, + #[serde(rename = "REDLIB_DEFAULT_HIDE_SCORE")] #[serde(alias = "LIBREDDIT_DEFAULT_HIDE_SCORE")] pub(crate) default_hide_score: Option, @@ -161,6 +165,7 @@ impl Config { default_hide_hls_notification: parse("REDLIB_DEFAULT_HIDE_HLS_NOTIFICATION"), default_hide_awards: parse("REDLIB_DEFAULT_HIDE_AWARDS"), default_hide_sidebar_and_summary: parse("REDLIB_DEFAULT_HIDE_SIDEBAR_AND_SUMMARY"), + default_hide_banner: parse("REDLIB_DEFAULT_HIDE_BANNER"), default_hide_score: parse("REDLIB_DEFAULT_HIDE_SCORE"), default_subscriptions: parse("REDLIB_DEFAULT_SUBSCRIPTIONS"), default_filters: parse("REDLIB_DEFAULT_FILTERS"), @@ -193,6 +198,7 @@ fn get_setting_from_config(name: &str, config: &Config) -> Option { "REDLIB_DEFAULT_WIDE" => config.default_wide.clone(), "REDLIB_DEFAULT_HIDE_AWARDS" => config.default_hide_awards.clone(), "REDLIB_DEFAULT_HIDE_SIDEBAR_AND_SUMMARY" => config.default_hide_sidebar_and_summary.clone(), + "REDLIB_DEFAULT_HIDE_BANNER" => config.default_hide_banner.clone(), "REDLIB_DEFAULT_HIDE_SCORE" => config.default_hide_score.clone(), "REDLIB_DEFAULT_SUBSCRIPTIONS" => config.default_subscriptions.clone(), "REDLIB_DEFAULT_FILTERS" => config.default_filters.clone(), diff --git a/src/settings.rs b/src/settings.rs index 6a65207..bed5c9c 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -21,7 +21,7 @@ struct SettingsTemplate { // CONSTANTS -const PREFS: [&str; 21] = [ +const PREFS: [&str; 22] = [ "theme", "mascot", "redsunlib_colorway", @@ -38,6 +38,7 @@ const PREFS: [&str; 21] = [ "hide_hls_notification", "autoplay_videos", "hide_sidebar_and_summary", + "hide_banner", "fixed_navbar", "hide_awards", "hide_score", diff --git a/src/utils.rs b/src/utils.rs index ba09800..c93c335 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -618,6 +618,7 @@ pub struct Preferences { pub hide_hls_notification: String, pub video_quality: String, pub hide_sidebar_and_summary: String, + pub hide_banner: String, pub use_hls: String, pub ffmpeg_video_downloads: String, pub autoplay_videos: String, @@ -671,6 +672,7 @@ impl Preferences { blur_spoiler: setting(req, "blur_spoiler"), show_nsfw: setting(req, "show_nsfw"), hide_sidebar_and_summary: setting(req, "hide_sidebar_and_summary"), + hide_banner: setting(req, "hide_banner"), blur_nsfw: setting(req, "blur_nsfw"), use_hls: setting(req, "use_hls"), ffmpeg_video_downloads: setting(req, "ffmpeg_video_downloads"), diff --git a/templates/settings.html b/templates/settings.html index a3bcd1c..5987e23 100644 --- a/templates/settings.html +++ b/templates/settings.html @@ -65,6 +65,11 @@
+
+ + + +
@@ -158,7 +163,7 @@

Note: settings and subscriptions are saved in browser cookies. Clearing your cookies will reset them.


-

You can restore your current settings and subscriptions after clearing your cookies using this link.

+

You can restore your current settings and subscriptions after clearing your cookies using this link.

{% if prefs.subscriptions.len() > 0 %}
From 138172b365d445e814ba46513a51b90040430556 Mon Sep 17 00:00:00 2001 From: ayaka Date: Mon, 13 Jan 2025 14:18:05 +1300 Subject: [PATCH 11/57] unnecessary log generation --- src/client.rs | 4 ++-- src/oauth.rs | 4 ++-- src/subreddit.rs | 3 +++ src/user.rs | 2 ++ 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/client.rs b/src/client.rs index 248fc88..ff3b580 100644 --- a/src/client.rs +++ b/src/client.rs @@ -7,7 +7,7 @@ use hyper::header::HeaderValue; use hyper::{body, body::Buf, header, Body, Client, Method, Request, Response, Uri}; use hyper_rustls::HttpsConnector; use libflate::gzip; -use log::{error, trace, warn}; +use log::{error, debug, warn}; use once_cell::sync::Lazy; use percent_encoding::{percent_encode, CONTROLS}; use serde_json::Value; @@ -396,7 +396,7 @@ pub async fn json(path: String, quarantine: bool) -> Result { response.headers().get("x-ratelimit-reset").and_then(|val| val.to_str().ok().map(|s| s.to_string())), response.headers().get("x-ratelimit-used").and_then(|val| val.to_str().ok().map(|s| s.to_string())), ) { - trace!( + debug!( "Ratelimit remaining: Header says {remaining}, we have {current_rate_limit}. Resets in {reset}. Rollover: {}. Ratelimit used: {used}", if is_rolling_over { "yes" } else { "no" }, ); diff --git a/src/oauth.rs b/src/oauth.rs index 80bf318..c68e50a 100644 --- a/src/oauth.rs +++ b/src/oauth.rs @@ -6,7 +6,7 @@ use crate::{ }; use base64::{engine::general_purpose, Engine as _}; use hyper::{client, Body, Method, Request}; -use log::{error, info, trace}; +use log::{error, info, debug, trace}; use serde_json::json; use tokio::time::{error::Elapsed, timeout}; @@ -160,7 +160,7 @@ pub async fn force_refresh_token() { return; } - trace!("Rolling over refresh token. Current rate limit: {}", OAUTH_RATELIMIT_REMAINING.load(Ordering::SeqCst)); + debug!("Rolling over refresh token. Current rate limit: {}", OAUTH_RATELIMIT_REMAINING.load(Ordering::SeqCst)); let new_client = Oauth::new().await; OAUTH_CLIENT.swap(new_client.into()); OAUTH_RATELIMIT_REMAINING.store(99, Ordering::SeqCst); diff --git a/src/subreddit.rs b/src/subreddit.rs index 5f964ef..1ae90d3 100644 --- a/src/subreddit.rs +++ b/src/subreddit.rs @@ -13,6 +13,7 @@ use rinja::Template; use once_cell::sync::Lazy; use regex::Regex; use time::{Duration, OffsetDateTime}; +use log::trace; // STRUCTS #[derive(Template)] @@ -461,6 +462,8 @@ async fn subreddit(sub: &str, quarantined: bool) -> Result { // Send a request to the url let res = json(path, quarantined).await?; + trace!("Subreddit info from r/{} : {}",sub,res["data"]); + // Metadata regarding the subreddit let members: i64 = res["data"]["subscribers"].as_u64().unwrap_or_default() as i64; let active: i64 = res["data"]["accounts_active"].as_u64().unwrap_or_default() as i64; diff --git a/src/user.rs b/src/user.rs index 50a4daa..5e8bfd7 100644 --- a/src/user.rs +++ b/src/user.rs @@ -8,6 +8,7 @@ use crate::{config, utils}; use hyper::{Body, Request, Response}; use rinja::Template; use time::{macros::format_description, OffsetDateTime}; +use log::trace; // STRUCTS #[derive(Template)] @@ -111,6 +112,7 @@ async fn user(name: &str) -> Result { // Send a request to the url json(path, false).await.map(|res| { + trace!("User info from r/{} : {}",name,res["data"]); // Grab creation date as unix timestamp let created_unix = res["data"]["created"].as_f64().unwrap_or(0.0).round() as i64; let created = OffsetDateTime::from_unix_timestamp(created_unix).unwrap_or(OffsetDateTime::UNIX_EPOCH); From 49afd83ad3d99c10f33a7fada861dcd55e09798a Mon Sep 17 00:00:00 2001 From: ayaka Date: Mon, 13 Jan 2025 14:41:49 +1300 Subject: [PATCH 12/57] subreddit-created-date --- src/subreddit.rs | 8 +++++++- src/utils.rs | 1 + static/style.css | 11 +++++------ templates/subreddit.html | 3 +++ 4 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/subreddit.rs b/src/subreddit.rs index 1ae90d3..f12bbb1 100644 --- a/src/subreddit.rs +++ b/src/subreddit.rs @@ -12,7 +12,8 @@ use rinja::Template; use once_cell::sync::Lazy; use regex::Regex; -use time::{Duration, OffsetDateTime}; +use time::{Duration, OffsetDateTime,macros::format_description}; + use log::trace; // STRUCTS @@ -467,6 +468,10 @@ async fn subreddit(sub: &str, quarantined: bool) -> Result { // Metadata regarding the subreddit let members: i64 = res["data"]["subscribers"].as_u64().unwrap_or_default() as i64; let active: i64 = res["data"]["accounts_active"].as_u64().unwrap_or_default() as i64; + + // Grab creation date as unix timestamp + let created_unix = res["data"]["created"].as_f64().unwrap_or(0.0).round() as i64; + let created = OffsetDateTime::from_unix_timestamp(created_unix).unwrap_or(OffsetDateTime::UNIX_EPOCH); // Fetch subreddit icon either from the community_icon or icon_img value let community_icon: &str = res["data"]["community_icon"].as_str().unwrap_or_default(); @@ -481,6 +486,7 @@ async fn subreddit(sub: &str, quarantined: bool) -> Result { icon: format_url(&icon), members: format_num(members), active: format_num(active), + created: created.format(format_description!("[month repr:short] [day] '[year repr:last_two]")).unwrap_or_default(), wiki: res["data"]["wiki_enabled"].as_bool().unwrap_or_default(), nsfw: res["data"]["over18"].as_bool().unwrap_or_default(), }) diff --git a/src/utils.rs b/src/utils.rs index b8105c3..dd8014e 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -587,6 +587,7 @@ pub struct Subreddit { pub icon: String, pub members: (String, String), pub active: (String, String), + pub created: String, pub wiki: bool, pub nsfw: bool, } diff --git a/static/style.css b/static/style.css index 9567a81..b37d7e2 100644 --- a/static/style.css +++ b/static/style.css @@ -531,11 +531,14 @@ aside { grid-template-columns: auto 4fr 1fr; } -#user_details, -#sub_details { +#user_details { display: grid; grid-template-columns: repeat(2, 1fr); } +#sub_details { + display: grid; + grid-template-columns: repeat(3, 1fr); +} @media screen and (max-width: 279px) { #sub_actions { display: unset; } } @@ -547,13 +550,9 @@ aside { /* Subscriptions */ -#sub_subscription, #user_subscription, -#sub_filter, #user_filter, -#sub_quicklist, #user_quicklist, -#sub_rss, #user_rss { margin-top: 20px; } diff --git a/templates/subreddit.html b/templates/subreddit.html index 5584863..8856fcd 100644 --- a/templates/subreddit.html +++ b/templates/subreddit.html @@ -120,9 +120,12 @@
+
{{ sub.members.0 }}
{{ sub.active.0 }}
+
{{ sub.created }}
+
{% if prefs.subscriptions.contains(sub.name) %} From 05e2c31bec6c072589ba2257870d45a789dfdc34 Mon Sep 17 00:00:00 2001 From: ayaka Date: Mon, 13 Jan 2025 15:10:35 +1300 Subject: [PATCH 13/57] #18 i seem to have left my brain somewhere --- templates/settings.html | 2 +- templates/subreddit.html | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/templates/settings.html b/templates/settings.html index 5987e23..d5391fa 100644 --- a/templates/settings.html +++ b/templates/settings.html @@ -66,7 +66,7 @@
- +
diff --git a/templates/subreddit.html b/templates/subreddit.html index c4f484b..21ea673 100644 --- a/templates/subreddit.html +++ b/templates/subreddit.html @@ -100,11 +100,13 @@ Wiki
{% endif %} - {% block head %} + {% if prefs.hide_banner != "on" %} + {% block head %} {% call super() %} - {% endblock %} -
+ {% endblock %} + {% endif %} +
Icon for r/{{ sub.name }}
From 4c516a7d570f3214e087d0ede5e0ff6d2d25d8b3 Mon Sep 17 00:00:00 2001 From: ayaka Date: Fri, 17 Jan 2025 23:05:53 +1300 Subject: [PATCH 14/57] cargo fmt --- src/client.rs | 2 +- src/instance_info.rs | 2 +- src/main.rs | 7 +++---- src/oauth.rs | 2 +- src/subreddit.rs | 9 ++++----- src/user.rs | 4 ++-- 6 files changed, 12 insertions(+), 14 deletions(-) diff --git a/src/client.rs b/src/client.rs index ff3b580..f74ab51 100644 --- a/src/client.rs +++ b/src/client.rs @@ -7,7 +7,7 @@ use hyper::header::HeaderValue; use hyper::{body, body::Buf, header, Body, Client, Method, Request, Response, Uri}; use hyper_rustls::HttpsConnector; use libflate::gzip; -use log::{error, debug, warn}; +use log::{debug, error, warn}; use once_cell::sync::Lazy; use percent_encoding::{percent_encode, CONTROLS}; use serde_json::Value; diff --git a/src/instance_info.rs b/src/instance_info.rs index a3bf32d..00a142f 100644 --- a/src/instance_info.rs +++ b/src/instance_info.rs @@ -148,7 +148,7 @@ impl InstanceInfo { ["Show NSFW", &convert(&self.config.default_show_nsfw)], ["Blur NSFW", &convert(&self.config.default_blur_nsfw)], ["Use HLS", &convert(&self.config.default_use_hls)], - ["Use FFmpeg", &convert(&self.config.default_ffmpeg_video_downloads)], + ["Use FFmpeg", &convert(&self.config.default_ffmpeg_video_downloads)], ["Hide HLS notification", &convert(&self.config.default_hide_hls_notification)], ["Subscriptions", &convert(&self.config.default_subscriptions)], ["Filters", &convert(&self.config.default_filters)], diff --git a/src/main.rs b/src/main.rs index d8a7d05..365624f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,7 +13,7 @@ use log::info; use once_cell::sync::Lazy; use redsunlib::client::{canonical_path, proxy, CLIENT}; use redsunlib::server::{self, RequestExt}; -use redsunlib::utils::{error, redirect, ThemeAssets, MascotAssets}; +use redsunlib::utils::{error, redirect, MascotAssets, ThemeAssets}; use redsunlib::{config, duplicates, headers, instance_info, post, search, settings, subreddit, user}; use redsunlib::client::OAUTH_CLIENT; @@ -110,8 +110,7 @@ async fn style() -> Result, String> { /// Serve mascot async fn mascot_image(req: Request) -> Result, String> { - let res = MascotAssets::get(&req.param("name").unwrap()) - .unwrap_or(MascotAssets::get("redsunlib.png").unwrap()); + let res = MascotAssets::get(&req.param("name").unwrap()).unwrap_or(MascotAssets::get("redsunlib.png").unwrap()); Ok( Response::builder() .status(200) @@ -249,7 +248,7 @@ async fn main() { app.at("/commits.json").get(|_| async move { proxy_commit_info().await }.boxed()); app.at("/instances.json").get(|_| async move { proxy_instances().await }.boxed()); - + // FFmpeg app .at("/ffmpeg/814.ffmpeg.js") diff --git a/src/oauth.rs b/src/oauth.rs index c68e50a..d15fa2f 100644 --- a/src/oauth.rs +++ b/src/oauth.rs @@ -6,7 +6,7 @@ use crate::{ }; use base64::{engine::general_purpose, Engine as _}; use hyper::{client, Body, Method, Request}; -use log::{error, info, debug, trace}; +use log::{debug, error, info, trace}; use serde_json::json; use tokio::time::{error::Elapsed, timeout}; diff --git a/src/subreddit.rs b/src/subreddit.rs index 86388e3..2eea14c 100644 --- a/src/subreddit.rs +++ b/src/subreddit.rs @@ -12,7 +12,7 @@ use rinja::Template; use once_cell::sync::Lazy; use regex::Regex; -use time::{Duration, OffsetDateTime,macros::format_description}; +use time::{macros::format_description, Duration, OffsetDateTime}; use log::trace; @@ -301,8 +301,7 @@ pub async fn subscriptions_filters_quicklists(req: Request) -> Result Result { // Send a request to the url let res = json(path, quarantined).await?; - trace!("Subreddit info from r/{} : {}",sub,res["data"]); + trace!("Subreddit info from r/{} : {}", sub, res["data"]); // Metadata regarding the subreddit let members: i64 = res["data"]["subscribers"].as_u64().unwrap_or_default() as i64; let active: i64 = res["data"]["accounts_active"].as_u64().unwrap_or_default() as i64; - + // Grab creation date as unix timestamp let created_unix = res["data"]["created"].as_f64().unwrap_or(0.0).round() as i64; let created = OffsetDateTime::from_unix_timestamp(created_unix).unwrap_or(OffsetDateTime::UNIX_EPOCH); diff --git a/src/user.rs b/src/user.rs index 5e8bfd7..6866700 100644 --- a/src/user.rs +++ b/src/user.rs @@ -6,9 +6,9 @@ use crate::server::RequestExt; use crate::utils::{error, filter_posts, format_url, get_filters, nsfw_landing, param, setting, template, Post, Preferences, User}; use crate::{config, utils}; use hyper::{Body, Request, Response}; +use log::trace; use rinja::Template; use time::{macros::format_description, OffsetDateTime}; -use log::trace; // STRUCTS #[derive(Template)] @@ -112,7 +112,7 @@ async fn user(name: &str) -> Result { // Send a request to the url json(path, false).await.map(|res| { - trace!("User info from r/{} : {}",name,res["data"]); + trace!("User info from r/{} : {}", name, res["data"]); // Grab creation date as unix timestamp let created_unix = res["data"]["created"].as_f64().unwrap_or(0.0).round() as i64; let created = OffsetDateTime::from_unix_timestamp(created_unix).unwrap_or(OffsetDateTime::UNIX_EPOCH); From 5c1e15c359e7b01316baab27d410130f1557cdf8 Mon Sep 17 00:00:00 2001 From: Butter Cat Date: Sun, 2 Feb 2025 21:48:46 -0500 Subject: [PATCH 15/57] Make subscription and filter cookies split into multiple cookies if they're too large (#288) * Split subscriptions and filters cookies into multiple cookies and make old cookies properly delete * Cleanup * Fix mispelling for removing subscription cookies * Fix many subscription misspellings * Fix subreddits and filters that were at the end and beginning of the cookies getting merged * Make join_until_size_limit take the +'s into account when calculating length * Start cookies without number to be backwards compatible * Fix old split cookies not being removed and subreddits/filters between cookies occasionally getting merged * Make updating subscription/filters cookies safer * Small cleanup * Make restore properly add new subscriptions/filters cookies and delete old unused subscriptions/filters cookies * Fix misspellings on variable name --- src/server.rs | 8 ++- src/settings.rs | 116 ++++++++++++++++++++++++++++++++++++++- src/subreddit.rs | 138 +++++++++++++++++++++++++++++++++++++++++------ src/utils.rs | 78 ++++++++++++++++++++++----- 4 files changed, 307 insertions(+), 33 deletions(-) diff --git a/src/server.rs b/src/server.rs index 15c56ad..e1f464d 100644 --- a/src/server.rs +++ b/src/server.rs @@ -25,7 +25,7 @@ use std::{ str::{from_utf8, Split}, string::ToString, }; -use time::Duration; +use time::OffsetDateTime; use crate::dbg_msg; @@ -170,10 +170,8 @@ impl ResponseExt for Response { } fn remove_cookie(&mut self, name: String) { - let mut cookie = Cookie::from(name); - cookie.set_path("/"); - cookie.set_max_age(Duration::seconds(1)); - if let Ok(val) = header::HeaderValue::from_str(&cookie.to_string()) { + let removal_cookie = Cookie::build(name).path("/").http_only(true).expires(OffsetDateTime::now_utc()); + if let Ok(val) = header::HeaderValue::from_str(&removal_cookie.to_string()) { self.headers_mut().append("Set-Cookie", val); } } diff --git a/src/settings.rs b/src/settings.rs index 4404912..34718c2 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -4,6 +4,7 @@ use std::collections::HashMap; // CRATES use crate::server::ResponseExt; +use crate::subreddit::join_until_size_limit; use crate::utils::{redirect, template, Preferences}; use cookie::Cookie; use futures_lite::StreamExt; @@ -119,7 +120,7 @@ fn set_cookies_method(req: Request, remove_cookies: bool) -> Response response.insert_cookie( Cookie::build((name.to_owned(), value.clone())) @@ -136,6 +137,119 @@ fn set_cookies_method(req: Request, remove_cookies: bool) -> Response = subscriptions.expect("Subscriptions").split('+').map(str::to_string).collect(); + + // Start at 0 to keep track of what number we need to start deleting old subscription cookies from + let mut subscriptions_number_to_delete_from = 0; + + // Starting at 0 so we handle the subscription cookie without a number first + for (subscriptions_number, list) in join_until_size_limit(&sub_list).into_iter().enumerate() { + let subscriptions_cookie = if subscriptions_number == 0 { + "subscriptions".to_string() + } else { + format!("subscriptions{}", subscriptions_number) + }; + + response.insert_cookie( + Cookie::build((subscriptions_cookie, list)) + .path("/") + .http_only(true) + .expires(OffsetDateTime::now_utc() + Duration::weeks(52)) + .into(), + ); + + subscriptions_number_to_delete_from += 1; + } + + // While subscriptionsNUMBER= is in the string of cookies add a response removing that cookie + while cookies_string.contains(&format!("subscriptions{subscriptions_number_to_delete_from}=")) { + // Remove that subscriptions cookie + response.remove_cookie(format!("subscriptions{subscriptions_number_to_delete_from}")); + + // Increment subscriptions cookie number + subscriptions_number_to_delete_from += 1; + } + } else { + // Remove unnumbered subscriptions cookie + response.remove_cookie("subscriptions".to_string()); + + // Starts at one to deal with the first numbered subscription cookie and onwards + let mut subscriptions_number_to_delete_from = 1; + + // While subscriptionsNUMBER= is in the string of cookies add a response removing that cookie + while cookies_string.contains(&format!("subscriptions{subscriptions_number_to_delete_from}=")) { + // Remove that subscriptions cookie + response.remove_cookie(format!("subscriptions{subscriptions_number_to_delete_from}")); + + // Increment subscriptions cookie number + subscriptions_number_to_delete_from += 1; + } + } + + // If there are filters to restore set them and delete any old filters cookies, otherwise delete them all + if filters.is_some() { + let filters_list: Vec = filters.expect("Filters").split('+').map(str::to_string).collect(); + + // Start at 0 to keep track of what number we need to start deleting old subscription cookies from + let mut filters_number_to_delete_from = 0; + + // Starting at 0 so we handle the subscription cookie without a number first + for (filters_number, list) in join_until_size_limit(&filters_list).into_iter().enumerate() { + let filters_cookie = if filters_number == 0 { + "filters".to_string() + } else { + format!("filters{}", filters_number) + }; + + response.insert_cookie( + Cookie::build((filters_cookie, list)) + .path("/") + .http_only(true) + .expires(OffsetDateTime::now_utc() + Duration::weeks(52)) + .into(), + ); + + filters_number_to_delete_from += 1; + } + + // While filtersNUMBER= is in the string of cookies add a response removing that cookie + while cookies_string.contains(&format!("filters{filters_number_to_delete_from}=")) { + // Remove that filters cookie + response.remove_cookie(format!("filters{filters_number_to_delete_from}")); + + // Increment filters cookie number + filters_number_to_delete_from += 1; + } + } else { + // Remove unnumbered filters cookie + response.remove_cookie("filters".to_string()); + + // Starts at one to deal with the first numbered subscription cookie and onwards + let mut filters_number_to_delete_from = 1; + + // While filtersNUMBER= is in the string of cookies add a response removing that cookie + while cookies_string.contains(&format!("filters{filters_number_to_delete_from}=")) { + // Remove that sfilters cookie + response.remove_cookie(format!("filters{filters_number_to_delete_from}")); + + // Increment filters cookie number + filters_number_to_delete_from += 1; + } + } + response } diff --git a/src/subreddit.rs b/src/subreddit.rs index 88aa542..2362a12 100644 --- a/src/subreddit.rs +++ b/src/subreddit.rs @@ -214,6 +214,41 @@ pub fn can_access_quarantine(req: &Request, sub: &str) -> bool { setting(req, &format!("allow_quaran_{}", sub.to_lowercase())).parse().unwrap_or_default() } +// Join items in chunks of 4000 bytes in length for cookies +pub fn join_until_size_limit(vec: &[T]) -> Vec { + let mut result = Vec::new(); + let mut list = String::new(); + let mut current_size = 0; + + for item in vec { + // Size in bytes + let item_size = item.to_string().len(); + // Use 4000 bytes to leave us some headroom because the name and options of the cookie count towards the 4096 byte cap + if current_size + item_size > 4000 { + // If last item add a seperator on the end of the list so it's interpreted properly in tanden with the next cookie + list.push('+'); + + // Push current list to result vector + result.push(list); + + // Reset the list variable so we can continue with only new items + list = String::new(); + } + // Add separator if not the first item + if !list.is_empty() { + list.push('+'); + } + // Add current item to list + list.push_str(&item.to_string()); + current_size = list.len() + item_size; + } + // Make sure to push whatever the remaining subreddits are there into the result vector + result.push(list); + + // Return resulting vector + result +} + // Sub, filter, unfilter, or unsub by setting subscription cookie using response "Set-Cookie" header pub async fn subscriptions_filters(req: Request) -> Result, String> { let sub = req.param("sub").unwrap_or_default(); @@ -306,28 +341,101 @@ pub async fn subscriptions_filters(req: Request) -> Result, let mut response = redirect(&path); - // Delete cookie if empty, else set + // If sub_list is empty remove all subscriptions cookies, otherwise update them and remove old ones if sub_list.is_empty() { + // Remove subscriptions cookie response.remove_cookie("subscriptions".to_string()); + + // Start with first numbered subscriptions cookie + let mut subscriptions_number = 1; + + // While whatever subscriptionsNUMBER cookie we're looking at has a value + while req.cookie(&format!("subscriptions{}", subscriptions_number)).is_some() { + // Remove that subscriptions cookie + response.remove_cookie(format!("subscriptions{}", subscriptions_number)); + + // Increment subscriptions cookie number + subscriptions_number += 1; + } } else { - response.insert_cookie( - Cookie::build(("subscriptions", sub_list.join("+"))) - .path("/") - .http_only(true) - .expires(OffsetDateTime::now_utc() + Duration::weeks(52)) - .into(), - ); + // Start at 0 to keep track of what number we need to start deleting old subscription cookies from + let mut subscriptions_number_to_delete_from = 0; + + // Starting at 0 so we handle the subscription cookie without a number first + for (subscriptions_number, list) in join_until_size_limit(&sub_list).into_iter().enumerate() { + let subscriptions_cookie = if subscriptions_number == 0 { + "subscriptions".to_string() + } else { + format!("subscriptions{}", subscriptions_number) + }; + + response.insert_cookie( + Cookie::build((subscriptions_cookie, list)) + .path("/") + .http_only(true) + .expires(OffsetDateTime::now_utc() + Duration::weeks(52)) + .into(), + ); + + subscriptions_number_to_delete_from += 1; + } + + // While whatever subscriptionsNUMBER cookie we're looking at has a value + while req.cookie(&format!("subscriptions{}", subscriptions_number_to_delete_from)).is_some() { + // Remove that subscriptions cookie + response.remove_cookie(format!("subscriptions{}", subscriptions_number_to_delete_from)); + + // Increment subscriptions cookie number + subscriptions_number_to_delete_from += 1; + } } + + // If filters is empty remove all filters cookies, otherwise update them and remove old ones if filters.is_empty() { + // Remove filters cookie response.remove_cookie("filters".to_string()); + + // Start with first numbered filters cookie + let mut filters_number = 1; + + // While whatever filtersNUMBER cookie we're looking at has a value + while req.cookie(&format!("filters{}", filters_number)).is_some() { + // Remove that filters cookie + response.remove_cookie(format!("filters{}", filters_number)); + + // Increment filters cookie number + filters_number += 1; + } } else { - response.insert_cookie( - Cookie::build(("filters", filters.join("+"))) - .path("/") - .http_only(true) - .expires(OffsetDateTime::now_utc() + Duration::weeks(52)) - .into(), - ); + // Start at 0 to keep track of what number we need to start deleting old filters cookies from + let mut filters_number_to_delete_from = 0; + + for (filters_number, list) in join_until_size_limit(&filters).into_iter().enumerate() { + let filters_cookie = if filters_number == 0 { + "filters".to_string() + } else { + format!("filters{}", filters_number) + }; + + response.insert_cookie( + Cookie::build((filters_cookie, list)) + .path("/") + .http_only(true) + .expires(OffsetDateTime::now_utc() + Duration::weeks(52)) + .into(), + ); + + filters_number_to_delete_from += 1; + } + + // While whatever filtersNUMBER cookie we're looking at has a value + while req.cookie(&format!("filters{}", filters_number_to_delete_from)).is_some() { + // Remove that filters cookie + response.remove_cookie(format!("filters{}", filters_number_to_delete_from)); + + // Increment filters cookie number + filters_number_to_delete_from += 1; + } } Ok(response) diff --git a/src/utils.rs b/src/utils.rs index c15dcea..e2cefd1 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -825,18 +825,72 @@ pub fn param(path: &str, value: &str) -> Option { // Retrieve the value of a setting by name pub fn setting(req: &Request, name: &str) -> String { // Parse a cookie value from request - req - .cookie(name) - .unwrap_or_else(|| { - // If there is no cookie for this setting, try receiving a default from the config - if let Some(default) = get_setting(&format!("REDLIB_DEFAULT_{}", name.to_uppercase())) { - Cookie::new(name, default) - } else { - Cookie::from(name) - } - }) - .value() - .to_string() + + // If this was called with "subscriptions" and the "subscriptions" cookie has a value + if name == "subscriptions" && req.cookie("subscriptions").is_some() { + // Create subscriptions string + let mut subscriptions = String::new(); + + // Default subscriptions cookie + if req.cookie("subscriptions").is_some() { + subscriptions.push_str(req.cookie("subscriptions").unwrap().value()); + } + + // Start with first numbered subscription cookie + let mut subscriptions_number = 1; + + // While whatever subscriptionsNUMBER cookie we're looking at has a value + while req.cookie(&format!("subscriptions{}", subscriptions_number)).is_some() { + // Push whatever subscriptionsNUMBER cookie we're looking at into the subscriptions string + subscriptions.push_str(req.cookie(&format!("subscriptions{}", subscriptions_number)).unwrap().value()); + + // Increment subscription cookie number + subscriptions_number += 1; + } + + // Return the subscriptions cookies as one large string + subscriptions + } + // If this was called with "filters" and the "filters" cookie has a value + else if name == "filters" && req.cookie("filters").is_some() { + // Create filters string + let mut filters = String::new(); + + // Default filters cookie + if req.cookie("filters").is_some() { + filters.push_str(req.cookie("filters").unwrap().value()); + } + + // Start with first numbered filters cookie + let mut filters_number = 1; + + // While whatever filtersNUMBER cookie we're looking at has a value + while req.cookie(&format!("filters{}", filters_number)).is_some() { + // Push whatever filtersNUMBER cookie we're looking at into the filters string + filters.push_str(req.cookie(&format!("filters{}", filters_number)).unwrap().value()); + + // Increment filters cookie number + filters_number += 1; + } + + // Return the filters cookies as one large string + filters + } + // The above two still come to this if there was no existing value + else { + req + .cookie(name) + .unwrap_or_else(|| { + // If there is no cookie for this setting, try receiving a default from the config + if let Some(default) = get_setting(&format!("REDLIB_DEFAULT_{}", name.to_uppercase())) { + Cookie::new(name, default) + } else { + Cookie::from(name) + } + }) + .value() + .to_string() + } } // Retrieve the value of a setting by name or the default value From 9e47bc37c7e3d1b5b929926d84459d5ca4a244a9 Mon Sep 17 00:00:00 2001 From: Kot C Date: Sun, 2 Feb 2025 20:49:46 -0600 Subject: [PATCH 16/57] Support HEAD requests (resolves #292) (#363) * Support HEAD requests * Remove body from error responses too --- src/server.rs | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/server.rs b/src/server.rs index e1f464d..5297c22 100644 --- a/src/server.rs +++ b/src/server.rs @@ -238,8 +238,14 @@ impl Server { path.pop(); } + // Replace HEAD with GET for routing + let (method, is_head) = match req.method() { + &Method::HEAD => (&Method::GET, true), + method => (method, false), + }; + // Match the visited path with an added route - match router.recognize(&format!("/{}{}", req.method().as_str(), path)) { + match router.recognize(&format!("/{}{}", method.as_str(), path)) { // If a route was configured for this path Ok(found) => { let mut parammed = req; @@ -251,17 +257,21 @@ impl Server { match func.await { Ok(mut res) => { res.headers_mut().extend(def_headers); - let _ = compress_response(&req_headers, &mut res).await; + if is_head { + *res.body_mut() = Body::empty(); + } else { + let _ = compress_response(&req_headers, &mut res).await; + } Ok(res) } - Err(msg) => new_boilerplate(def_headers, req_headers, 500, Body::from(msg)).await, + Err(msg) => new_boilerplate(def_headers, req_headers, 500, if is_head { Body::empty() } else { Body::from(msg) }).await, } } .boxed() } // If there was a routing error - Err(e) => new_boilerplate(def_headers, req_headers, 404, e.into()).boxed(), + Err(e) => new_boilerplate(def_headers, req_headers, 404, if is_head { Body::empty() } else { e.into() }).boxed(), } })) } From adf25cb15b61984581422ac798cc7c1364ad8e75 Mon Sep 17 00:00:00 2001 From: Martin Lindhe Date: Mon, 3 Feb 2025 03:56:47 +0100 Subject: [PATCH 17/57] unescape selftext_html from json api, fixes #354 (#357) * unescape selftext_html from json api, fixes #354 * fix(fmt) --------- Co-authored-by: Matthew Esposito --- Cargo.lock | 7 +++++++ Cargo.toml | 1 + src/utils.rs | 3 ++- 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 819d4bc..20d528b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -770,6 +770,12 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" +[[package]] +name = "htmlescape" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9025058dae765dee5070ec375f591e2ba14638c63feff74f13805a72e523163" + [[package]] name = "http" version = "0.2.12" @@ -1367,6 +1373,7 @@ dependencies = [ "dotenvy", "fastrand", "futures-lite", + "htmlescape", "hyper", "hyper-rustls", "libflate", diff --git a/Cargo.toml b/Cargo.toml index a1d3ec0..a4d0170 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,6 +51,7 @@ common-words-all = { version = "0.0.2", default-features = false, features = ["e hyper-rustls = { version = "0.24.2", features = [ "http2" ] } tegen = "0.1.4" serde_urlencoded = "0.7.1" +htmlescape = "0.3.1" [dev-dependencies] diff --git a/src/utils.rs b/src/utils.rs index e2cefd1..ea14dac 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -7,6 +7,7 @@ use crate::config::{self, get_setting}; // use crate::{client::json, server::RequestExt}; use cookie::Cookie; +use htmlescape::decode_html; use hyper::{Body, Request, Response}; use log::error; use once_cell::sync::Lazy; @@ -376,7 +377,7 @@ impl Post { let awards = Awards::parse(&data["all_awardings"]); // selftext_html is set for text posts when browsing. - let mut body = rewrite_urls(&val(post, "selftext_html")); + let mut body = rewrite_urls(&decode_html(&val(post, "selftext_html")).unwrap()); if body.is_empty() { body = rewrite_urls(&val(post, "body_html")); } From fd1c32f5552cc116e1cb4c95fcd7cc7a7b069335 Mon Sep 17 00:00:00 2001 From: Martin Lindhe Date: Mon, 3 Feb 2025 04:00:44 +0100 Subject: [PATCH 18/57] rss: add field, fixes #356 (#358) * rss: add field, fixes #356 * rss: also add pub_date on user feed * fix(fmt) --------- Co-authored-by: Matthew Esposito --- Cargo.lock | 5 +++-- Cargo.toml | 1 + src/subreddit.rs | 2 ++ src/user.rs | 2 ++ 4 files changed, 8 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 20d528b..24791b4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -274,9 +274,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.38" +version = "0.4.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" dependencies = [ "num-traits", ] @@ -1367,6 +1367,7 @@ dependencies = [ "brotli", "build_html", "cached", + "chrono", "clap", "common-words-all", "cookie", diff --git a/Cargo.toml b/Cargo.toml index a4d0170..843b9c9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,6 +51,7 @@ common-words-all = { version = "0.0.2", default-features = false, features = ["e hyper-rustls = { version = "0.24.2", features = [ "http2" ] } tegen = "0.1.4" serde_urlencoded = "0.7.1" +chrono = { version = "0.4.39", default-features = false, features = [ "std" ] } htmlescape = "0.3.1" diff --git a/src/subreddit.rs b/src/subreddit.rs index 2362a12..d5d5196 100644 --- a/src/subreddit.rs +++ b/src/subreddit.rs @@ -11,6 +11,7 @@ use hyper::{Body, Request, Response}; use log::{debug, trace}; use rinja::Template; +use chrono::DateTime; use once_cell::sync::Lazy; use regex::Regex; use time::{Duration, OffsetDateTime}; @@ -607,6 +608,7 @@ pub async fn rss(req: Request) -> Result, String> { link: Some(utils::get_post_url(&post)), author: Some(post.author.name), content: Some(rewrite_urls(&post.body)), + pub_date: Some(DateTime::from_timestamp(post.created_ts as i64, 0).unwrap_or_default().to_rfc2822()), description: Some(format!( "Comments", config::get_setting("REDLIB_FULL_URL").unwrap_or_default(), diff --git a/src/user.rs b/src/user.rs index 50a4daa..2fb8b0d 100644 --- a/src/user.rs +++ b/src/user.rs @@ -5,6 +5,7 @@ use crate::client::json; use crate::server::RequestExt; use crate::utils::{error, filter_posts, format_url, get_filters, nsfw_landing, param, setting, template, Post, Preferences, User}; use crate::{config, utils}; +use chrono::DateTime; use hyper::{Body, Request, Response}; use rinja::Template; use time::{macros::format_description, OffsetDateTime}; @@ -165,6 +166,7 @@ pub async fn rss(req: Request) -> Result, String> { title: Some(post.title.to_string()), link: Some(utils::get_post_url(&post)), author: Some(post.author.name), + pub_date: Some(DateTime::from_timestamp(post.created_ts as i64, 0).unwrap_or_default().to_rfc2822()), content: Some(rewrite_urls(&post.body)), ..Default::default() }) From cb659cc8a3fa8e85e7fcd6c5e96d67fa83081c7b Mon Sep 17 00:00:00 2001 From: Martin Lindhe Date: Mon, 3 Feb 2025 04:00:58 +0100 Subject: [PATCH 19/57] rss: proxy links in users and subreddit feeds, fixes #359 (#361) --- src/subreddit.rs | 2 +- src/user.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/subreddit.rs b/src/subreddit.rs index d5d5196..0db4f77 100644 --- a/src/subreddit.rs +++ b/src/subreddit.rs @@ -605,7 +605,7 @@ pub async fn rss(req: Request) -> Result, String> { .into_iter() .map(|post| Item { title: Some(post.title.to_string()), - link: Some(utils::get_post_url(&post)), + link: Some(format_url(&utils::get_post_url(&post))), author: Some(post.author.name), content: Some(rewrite_urls(&post.body)), pub_date: Some(DateTime::from_timestamp(post.created_ts as i64, 0).unwrap_or_default().to_rfc2822()), diff --git a/src/user.rs b/src/user.rs index 2fb8b0d..818f368 100644 --- a/src/user.rs +++ b/src/user.rs @@ -164,7 +164,7 @@ pub async fn rss(req: Request) -> Result, String> { .into_iter() .map(|post| Item { title: Some(post.title.to_string()), - link: Some(utils::get_post_url(&post)), + link: Some(format_url(&utils::get_post_url(&post))), author: Some(post.author.name), pub_date: Some(DateTime::from_timestamp(post.created_ts as i64, 0).unwrap_or_default().to_rfc2822()), content: Some(rewrite_urls(&post.body)), From 0703fa103611786637328bf569928c0619aff759 Mon Sep 17 00:00:00 2001 From: Vivek Date: Sun, 2 Feb 2025 19:10:12 -0800 Subject: [PATCH 20/57] [build] add new dockerfiles for building from source (#244) * add new dockerfiles * update default ubuntu base images * updates * update comment * update cargo command Co-authored-by: Pim * update cargo command Co-authored-by: Pim * specify binary * use label instead of maintainer --------- Co-authored-by: Pim --- Dockerfile.alpine | 45 +++++++++++++++++++++++++++++++++++++++++ Dockerfile.ubuntu | 51 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+) create mode 100644 Dockerfile.alpine create mode 100644 Dockerfile.ubuntu diff --git a/Dockerfile.alpine b/Dockerfile.alpine new file mode 100644 index 0000000..051476a --- /dev/null +++ b/Dockerfile.alpine @@ -0,0 +1,45 @@ +# supported versions here: https://hub.docker.com/_/rust +ARG ALPINE_VERSION=3.20 + +######################## +## builder image +######################## +FROM rust:alpine${ALPINE_VERSION} AS builder + +RUN apk add --no-cache musl-dev + +WORKDIR /redlib + +# download (most) dependencies in their own layer +COPY Cargo.lock Cargo.toml ./ +RUN mkdir src && echo "fn main() { panic!(\"why am i running?\") }" > src/main.rs +RUN cargo build --release --locked --bin redlib +RUN rm ./src/main.rs && rmdir ./src + +# copy the source and build the redlib binary +COPY . ./ +RUN cargo build --release --locked --bin redlib +RUN echo "finished building redlib!" + +######################## +## release image +######################## +FROM alpine:${ALPINE_VERSION} AS release + +# Import redlib binary from builder +COPY --from=builder /redlib/target/release/redlib /usr/local/bin/redlib + +# Add non-root user for running redlib +RUN adduser --home /nonexistent --no-create-home --disabled-password redlib +USER redlib + +# Document that we intend to expose port 8080 to whoever runs the container +EXPOSE 8080 + +# Run a healthcheck every minute to make sure redlib is functional +HEALTHCHECK --interval=1m --timeout=3s CMD wget --spider --q http://localhost:8080/settings || exit 1 + +# Add container metadata +LABEL org.opencontainers.image.authors="sigaloid" + +CMD ["redlib"] diff --git a/Dockerfile.ubuntu b/Dockerfile.ubuntu new file mode 100644 index 0000000..2e277c5 --- /dev/null +++ b/Dockerfile.ubuntu @@ -0,0 +1,51 @@ +# supported versions here: https://hub.docker.com/_/rust +ARG RUST_BUILDER_VERSION=slim-bookworm +ARG UBUNTU_RELEASE_VERSION=noble + +######################## +## builder image +######################## +FROM rust:${RUST_BUILDER_VERSION} AS builder + +WORKDIR /redlib + +# download (most) dependencies in their own layer +COPY Cargo.lock Cargo.toml ./ +RUN mkdir src && echo "fn main() { panic!(\"why am i running?\") }" > src/main.rs +RUN cargo build --release --locked --bin redlib +RUN rm ./src/main.rs && rmdir ./src + +# copy the source and build the redlib binary +COPY . ./ +RUN cargo build --release --locked --bin redlib +RUN echo "finished building redlib!" + +######################## +## release image +######################## +FROM ubuntu:${UBUNTU_RELEASE_VERSION} AS release + +# Install ca-certificates +RUN apt-get update && apt-get install -y ca-certificates + +# Import redlib binary from builder +COPY --from=builder /redlib/target/release/redlib /usr/local/bin/redlib + +# Add non-root user for running redlib +RUN useradd \ + --no-create-home \ + --password "!" \ + --comment "user for running redlib" \ + redlib +USER redlib + +# Document that we intend to expose port 8080 to whoever runs the container +EXPOSE 8080 + +# Run a healthcheck every minute to make sure redlib is functional +HEALTHCHECK --interval=1m --timeout=3s CMD wget --spider --q http://localhost:8080/settings || exit 1 + +# Add container metadata +LABEL org.opencontainers.image.authors="sigaloid" + +CMD ["redlib"] From 9e39a75e82cbf0c83b09e051c13073fa4a5e3f5a Mon Sep 17 00:00:00 2001 From: Joel Koen Date: Mon, 3 Feb 2025 14:16:59 +1000 Subject: [PATCH 21/57] build(nix): update deps (#331) --- flake.lock | 32 ++++++++++++-------------------- flake.nix | 10 ++-------- 2 files changed, 14 insertions(+), 28 deletions(-) diff --git a/flake.lock b/flake.lock index 4569244..2b0b585 100644 --- a/flake.lock +++ b/flake.lock @@ -1,17 +1,12 @@ { "nodes": { "crane": { - "inputs": { - "nixpkgs": [ - "nixpkgs" - ] - }, "locked": { - "lastModified": 1717025063, - "narHash": "sha256-dIubLa56W9sNNz0e8jGxrX3CAkPXsq7snuFA/Ie6dn8=", + "lastModified": 1731974733, + "narHash": "sha256-enYSSZVVl15FI5p+0Y5/Ckf5DZAvXe6fBrHxyhA/njc=", "owner": "ipetkov", "repo": "crane", - "rev": "480dff0be03dac0e51a8dfc26e882b0d123a450e", + "rev": "3cb338ce81076ce5e461cf77f7824476addb0e1c", "type": "github" }, "original": { @@ -25,11 +20,11 @@ "systems": "systems" }, "locked": { - "lastModified": 1710146030, - "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", "owner": "numtide", "repo": "flake-utils", - "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", "type": "github" }, "original": { @@ -40,11 +35,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1717112898, - "narHash": "sha256-7R2ZvOnvd9h8fDd65p0JnB7wXfUvreox3xFdYWd1BnY=", + "lastModified": 1731890469, + "narHash": "sha256-D1FNZ70NmQEwNxpSSdTXCSklBH1z2isPR84J6DQrJGs=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "6132b0f6e344ce2fe34fc051b72fb46e34f668e0", + "rev": "5083ec887760adfe12af64830a66807423a859a7", "type": "github" }, "original": { @@ -64,19 +59,16 @@ }, "rust-overlay": { "inputs": { - "flake-utils": [ - "flake-utils" - ], "nixpkgs": [ "nixpkgs" ] }, "locked": { - "lastModified": 1717121863, - "narHash": "sha256-/3sxIe7MZqF/jw1RTQCSmgTjwVod43mmrk84m50MJQ4=", + "lastModified": 1732069891, + "narHash": "sha256-moKx8AVJrViCSdA0e0nSsG8b1dAsObI4sRAtbqbvBY8=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "2a7b53172ed08f856b8382d7dcfd36a4e0cbd866", + "rev": "8509a51241c407d583b1963d5079585a992506e8", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 8bcacf6..0180c8d 100644 --- a/flake.nix +++ b/flake.nix @@ -4,19 +4,13 @@ inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; - crane = { - url = "github:ipetkov/crane"; - inputs.nixpkgs.follows = "nixpkgs"; - }; + crane.url = "github:ipetkov/crane"; flake-utils.url = "github:numtide/flake-utils"; rust-overlay = { url = "github:oxalica/rust-overlay"; - inputs = { - nixpkgs.follows = "nixpkgs"; - flake-utils.follows = "flake-utils"; - }; + inputs.nixpkgs.follows = "nixpkgs"; }; }; From 96ad7bf1632ef4bafeba148288422ffe4cf9bfb2 Mon Sep 17 00:00:00 2001 From: mooons <10822203+mooons@users.noreply.github.com> Date: Sun, 2 Feb 2025 20:26:36 -0800 Subject: [PATCH 22/57] feat: render bullet lists (#321) * feat: render bullet lists * tests: add tests --------- Co-authored-by: Matthew Esposito --- src/utils.rs | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/src/utils.rs b/src/utils.rs index ea14dac..1bc70b9 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -989,6 +989,17 @@ pub fn format_url(url: &str) -> String { } } +static REGEX_BULLET: Lazy = Lazy::new(|| Regex::new(r"(?m)^- (.*)$").unwrap()); +static REGEX_BULLET_CONSECUTIVE_LINES: Lazy = Lazy::new(|| Regex::new(r"\n
    ").unwrap()); + +pub fn render_bullet_lists(input_text: &str) -> String { + // ref: https://stackoverflow.com/a/4902622 + // First enclose each bullet with
    • tags + let text1 = REGEX_BULLET.replace_all(&input_text, "
      • $1
      ").to_string(); + // Then remove any consecutive
      tags + REGEX_BULLET_CONSECUTIVE_LINES.replace_all(&text1, "").to_string() +} + // These are links we want to replace in-body static REDDIT_REGEX: Lazy = Lazy::new(|| Regex::new(r#"href="(https|http|)://(www\.|old\.|np\.|amp\.|new\.|)(reddit\.com|redd\.it)/"#).unwrap()); static REDDIT_PREVIEW_REGEX: Lazy = Lazy::new(|| Regex::new(r"https?://(external-preview|preview|i)\.redd\.it(.*)[^?]").unwrap()); @@ -1143,6 +1154,10 @@ pub fn rewrite_emotes(media_metadata: &Value, comment: String) -> String { } } } + + // render bullet (unordered) lists + comment = render_bullet_lists(&comment); + // Call rewrite_urls() to transform any other Reddit links rewrite_urls(&comment) } @@ -1505,3 +1520,28 @@ fn test_rewriting_emotes() { let output = r#"

      "#; assert_eq!(rewrite_emotes(&json_input, comment_input.to_string()), output); } + +#[test] +fn test_rewriting_bullet_list() { + let input = r#"

      Hi, I've bought this very same monitor and found no calibration whatsoever. I have an ICC profile that has been set up since I've installed its driver from the LG website and it works ok. I also used http://www.lagom.nl/lcd-test/ to calibrate it. After some good tinkering I've found the following settings + the color profile from the driver gets me past all the tests perfectly: +- Brightness 50 (still have to settle on this one, it's personal preference, it controls the backlight, not the colors) +- Contrast 70 (which for me was the default one) +- Picture mode Custom +- Super resolution + Off (it looks horrible anyway) +- Sharpness 50 (default one I think) +- Black level High (low messes up gray colors) +- DFC Off +- Response Time Middle (personal preference, https://www.blurbusters.com/ show horrible overdrive with it on high) +- Freesync doesn't matter +- Black stabilizer 50 +- Gamma setting on 0 +- Color Temp Medium +How`s your monitor by the way? Any IPS bleed whatsoever? I either got lucky or the panel is pretty good, 0 bleed for me, just the usual IPS glow. How about the pixels? I see the pixels even at one meter away, especially on Microsoft Edge's icon for example, the blue background is just blocky, don't know why.

      +
      "#; +let output = r#"

      Hi, I've bought this very same monitor and found no calibration whatsoever. I have an ICC profile that has been set up since I've installed its driver from the LG website and it works ok. I also used http://www.lagom.nl/lcd-test/ to calibrate it. After some good tinkering I've found the following settings + the color profile from the driver gets me past all the tests perfectly: +

      • Brightness 50 (still have to settle on this one, it's personal preference, it controls the backlight, not the colors)
      • Contrast 70 (which for me was the default one)
      • Picture mode Custom
      • Super resolution + Off (it looks horrible anyway)
      • Sharpness 50 (default one I think)
      • Black level High (low messes up gray colors)
      • DFC Off
      • Response Time Middle (personal preference, https://www.blurbusters.com/ show horrible overdrive with it on high)
      • Freesync doesn't matter
      • Black stabilizer 50
      • Gamma setting on 0
      • Color Temp Medium
      +How`s your monitor by the way? Any IPS bleed whatsoever? I either got lucky or the panel is pretty good, 0 bleed for me, just the usual IPS glow. How about the pixels? I see the pixels even at one meter away, especially on Microsoft Edge's icon for example, the blue background is just blocky, don't know why.

      +
      "#; + + assert_eq!(render_bullet_lists(input), output); +} \ No newline at end of file From 23cda23d013e4a2866afd1cad0649cb40bb612d0 Mon Sep 17 00:00:00 2001 From: Matthew Esposito Date: Sun, 2 Feb 2025 23:30:33 -0500 Subject: [PATCH 23/57] feat: add environment variables and dedicated flags for ipv4/6 only (#307) * feat: add environment variables and dedicated flags for ipv4/6 only * fix(readme): mention all flags on README --- README.md | 11 +++++++++++ src/main.rs | 25 ++++++++++++++++++++++++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 4026791..6fabfcc 100644 --- a/README.md +++ b/README.md @@ -404,6 +404,17 @@ REDLIB_DEFAULT_USE_HLS = "on" > > If using Docker Compose, no changes are needed as the `.env` file is already referenced in `compose.yaml` via the `env_file: .env` line. +## Command Line Flags + +Redlib supports the following command line flags: + +- `-4`, `--ipv4-only`: Listen on IPv4 only. +- `-6`, `--ipv6-only`: Listen on IPv6 only. +- `-r`, `--redirect-https`: Redirect all HTTP requests to HTTPS (no longer functional). +- `-a`, `--address
      `: Sets address to listen on. Default is `[::]`. +- `-p`, `--port `: Port to listen on. Default is `8080`. +- `-H`, `--hsts `: HSTS header to tell browsers that this site should only be accessed over HTTPS. Default is `604800`. + ## Instance settings Assign a default value for each instance-specific setting by passing environment variables to Redlib in the format `REDLIB_{X}`. Replace `{X}` with the setting name (see list below) in capital letters. diff --git a/src/main.rs b/src/main.rs index 8732d20..a109560 100644 --- a/src/main.rs +++ b/src/main.rs @@ -108,6 +108,20 @@ async fn main() { let matches = Command::new("Redlib") .version(env!("CARGO_PKG_VERSION")) .about("Private front-end for Reddit written in Rust ") + .arg( + Arg::new("ipv4-only") + .short('4') + .long("ipv4-only") + .help("Listen on IPv4 only") + .num_args(0), + ) + .arg( + Arg::new("ipv6-only") + .short('6') + .long("ipv6-only") + .help("Listen on IPv6 only") + .num_args(0), + ) .arg( Arg::new("redirect-https") .short('r') @@ -164,7 +178,16 @@ async fn main() { let port = matches.get_one::("port").unwrap(); let hsts = matches.get_one("hsts").map(|m: &String| m.as_str()); - let listener = [address, ":", port].concat(); + let ipv4_only = std::env::var("IPV4_ONLY").is_ok() || matches.get_flag("ipv4-only"); + let ipv6_only = std::env::var("IPV6_ONLY").is_ok() || matches.get_flag("ipv6-only"); + + let listener = if ipv4_only { + format!("0.0.0.0:{}", port) + } else if ipv6_only { + format!("[::]:{}", port) + } else { + [address, ":", port].concat() + }; println!("Starting Redlib..."); From 2e0e1a1aaab5e020365e06343e5d9def30a46466 Mon Sep 17 00:00:00 2001 From: Butter Cat Date: Sun, 2 Feb 2025 23:31:37 -0500 Subject: [PATCH 24/57] Fix crossposted galleries not working (#293) --- src/utils.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/utils.rs b/src/utils.rs index 1bc70b9..c4991d8 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -234,6 +234,14 @@ impl Media { // If this post contains a gallery of images gallery = GalleryMedia::parse(&data["gallery_data"]["items"], &data["media_metadata"]); + ("gallery", &data["url"], None) + } else if data["crosspost_parent_list"][0]["is_gallery"].as_bool().unwrap_or_default() { + // If this post contains a gallery of images + gallery = GalleryMedia::parse( + &data["crosspost_parent_list"][0]["gallery_data"]["items"], + &data["crosspost_parent_list"][0]["media_metadata"], + ); + ("gallery", &data["url"], None) } else if data["is_reddit_media_domain"].as_bool().unwrap_or_default() && data["domain"] == "i.redd.it" { // If this post contains a reddit media (image) URL. From 68a0517115b31d5188c01d0c5c30575bf877a6a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Val=C3=A9rio?= Date: Mon, 3 Feb 2025 04:32:23 +0000 Subject: [PATCH 25/57] update devcontainer image, that includes a more recent version of rust (#294) --- .devcontainer/devcontainer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 3a941de..3ec1ead 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,6 +1,6 @@ { "name": "Rust", - "image": "mcr.microsoft.com/devcontainers/rust:0-1-bullseye", + "image": "mcr.microsoft.com/devcontainers/rust:1.0.9-bookworm", "features": { "ghcr.io/devcontainers/features/docker-in-docker:2": {} }, From 51386671d3474b5020cd43db8d7536ba1ac7979d Mon Sep 17 00:00:00 2001 From: Butter Cat Date: Sun, 2 Feb 2025 23:38:52 -0500 Subject: [PATCH 26/57] Fix embedded images sometimes having gaps around them (#295) * Fix images embedded by rewrite_urls() having an empty

      above and below them that caused weird gaps in some scenarios * Fix test for new embedding behavior * fix: remove println --------- Co-authored-by: Matthew Esposito --- src/utils.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/utils.rs b/src/utils.rs index c4991d8..4ed7664 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1010,7 +1010,7 @@ pub fn render_bullet_lists(input_text: &str) -> String { // These are links we want to replace in-body static REDDIT_REGEX: Lazy = Lazy::new(|| Regex::new(r#"href="(https|http|)://(www\.|old\.|np\.|amp\.|new\.|)(reddit\.com|redd\.it)/"#).unwrap()); -static REDDIT_PREVIEW_REGEX: Lazy = Lazy::new(|| Regex::new(r"https?://(external-preview|preview|i)\.redd\.it(.*)[^?]").unwrap()); +static REDDIT_PREVIEW_REGEX: Lazy = Lazy::new(|| Regex::new(r"https?://(external-preview|preview|i)\.redd\.it(.*)").unwrap()); static REDDIT_EMOJI_REGEX: Lazy = Lazy::new(|| Regex::new(r"https?://(www|).redditstatic\.com/(.*)").unwrap()); static REDLIB_PREVIEW_LINK_REGEX: Lazy = Lazy::new(|| Regex::new(r#"/(img|preview/)(pre|external-pre)?/(.*?)>"#).unwrap()); static REDLIB_PREVIEW_TEXT_REGEX: Lazy = Lazy::new(|| Regex::new(r">(.*?)").unwrap()); @@ -1052,7 +1052,7 @@ pub fn rewrite_urls(input_text: &str) -> String { } // image_url contains > at the end of it, and right above this we remove image_text's front >, leaving us with just a single > between them - let image_to_replace = format!(""); + let image_to_replace = format!("

      "); // _image_replacement needs to be in scope for the replacement at the bottom of the loop let mut _image_replacement = String::new(); @@ -1501,7 +1501,7 @@ async fn test_fetching_ws() { fn test_rewriting_image_links() { let input = r#"

      caption 1

      "#; - let output = r#"

      caption 1
      caption 1
      "#; assert_eq!(rewrite_urls(input), output); } From bbe5f8191475ea20e41e5a4eb1572fbd2a45d8b3 Mon Sep 17 00:00:00 2001 From: internationalcrisis <97415475+internationalcrisis@users.noreply.github.com> Date: Sun, 2 Feb 2025 22:40:19 -0600 Subject: [PATCH 27/57] fix: gracefully shutdown on CTRL+C and SIGTERM (#273) Fixes #205 --- src/server.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/server.rs b/src/server.rs index 5297c22..a287de2 100644 --- a/src/server.rs +++ b/src/server.rs @@ -282,8 +282,19 @@ impl Server { // Bind server to address specified above. Gracefully shut down if CTRL+C is pressed let server = HyperServer::bind(address).serve(make_svc).with_graceful_shutdown(async { + #[cfg(windows)] // Wait for the CTRL+C signal tokio::signal::ctrl_c().await.expect("Failed to install CTRL+C signal handler"); + + #[cfg(unix)] + { + // Wait for CTRL+C or SIGTERM signals + let mut signal_terminate = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()).expect("Failed to install SIGTERM signal handler"); + tokio::select! { + _ = tokio::signal::ctrl_c() => (), + _ = signal_terminate.recv() => () + } + } }); server.boxed() From 257871b56ca1d5612482df05fed14b1c45e04de9 Mon Sep 17 00:00:00 2001 From: Matthew Esposito Date: Mon, 3 Feb 2025 00:30:48 -0500 Subject: [PATCH 28/57] fix(tests) --- src/utils.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/utils.rs b/src/utils.rs index 4ed7664..cf226b9 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1483,7 +1483,10 @@ async fn test_fetching_subreddit_quarantined() { #[tokio::test(flavor = "multi_thread")] async fn test_fetching_nsfw_subreddit() { - let subreddit = Post::fetch("/r/randnsfw", false).await; + // Gonwild is a place for closed, Euclidean Geometric shapes to exchange their nth terms for karma; showing off their edges in a comfortable environment without pressure. + // Find a good sub that is tagged NSFW but that actually isn't in case my future employers are watching (they probably are) + // switched from randnsfw as it is no longer functional. + let subreddit = Post::fetch("/r/gonwild", false).await; assert!(subreddit.is_ok()); assert!(!subreddit.unwrap().0.is_empty()); } From 7930b19809f00c99705949027550add74068da3b Mon Sep 17 00:00:00 2001 From: Matthew Esposito Date: Mon, 3 Feb 2025 00:47:25 -0500 Subject: [PATCH 29/57] fix: fix clippy + tests --- src/client.rs | 6 ------ src/main.rs | 18 +++--------------- src/utils.rs | 4 ++-- 3 files changed, 5 insertions(+), 23 deletions(-) diff --git a/src/client.rs b/src/client.rs index fa32fc0..76369ca 100644 --- a/src/client.rs +++ b/src/client.rs @@ -544,12 +544,6 @@ async fn test_obfuscated_share_link() { assert_eq!(canonical_path(share_link, 3).await, Ok(Some(canonical_link))); } -#[tokio::test(flavor = "multi_thread")] -async fn test_share_link_strip_json() { - let link = "/17krzvz".into(); - let canonical_link = "/comments/17krzvz".into(); - assert_eq!(canonical_path(link, 3).await, Ok(Some(canonical_link))); -} #[tokio::test(flavor = "multi_thread")] async fn test_private_sub() { let link = json("/r/suicide/about.json?raw_json=1".into(), true).await; diff --git a/src/main.rs b/src/main.rs index a109560..165f0cb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -108,20 +108,8 @@ async fn main() { let matches = Command::new("Redlib") .version(env!("CARGO_PKG_VERSION")) .about("Private front-end for Reddit written in Rust ") - .arg( - Arg::new("ipv4-only") - .short('4') - .long("ipv4-only") - .help("Listen on IPv4 only") - .num_args(0), - ) - .arg( - Arg::new("ipv6-only") - .short('6') - .long("ipv6-only") - .help("Listen on IPv6 only") - .num_args(0), - ) + .arg(Arg::new("ipv4-only").short('4').long("ipv4-only").help("Listen on IPv4 only").num_args(0)) + .arg(Arg::new("ipv6-only").short('6').long("ipv6-only").help("Listen on IPv6 only").num_args(0)) .arg( Arg::new("redirect-https") .short('r') @@ -392,7 +380,7 @@ async fn main() { Some("best" | "hot" | "new" | "top" | "rising" | "controversial") => subreddit::community(req).await, // Short link for post - Some(id) if (5..8).contains(&id.len()) => match canonical_path(format!("/{id}"), 3).await { + Some(id) if (5..8).contains(&id.len()) => match canonical_path(format!("/comments/{id}"), 3).await { Ok(path_opt) => match path_opt { Some(path) => Ok(redirect(&path)), None => error(req, "Post ID is invalid. It may point to a post on a community that has been banned.").await, diff --git a/src/utils.rs b/src/utils.rs index cf226b9..867f73f 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1549,10 +1549,10 @@ fn test_rewriting_bullet_list() { - Color Temp Medium How`s your monitor by the way? Any IPS bleed whatsoever? I either got lucky or the panel is pretty good, 0 bleed for me, just the usual IPS glow. How about the pixels? I see the pixels even at one meter away, especially on Microsoft Edge's icon for example, the blue background is just blocky, don't know why.

"#; -let output = r#"

Hi, I've bought this very same monitor and found no calibration whatsoever. I have an ICC profile that has been set up since I've installed its driver from the LG website and it works ok. I also used http://www.lagom.nl/lcd-test/ to calibrate it. After some good tinkering I've found the following settings + the color profile from the driver gets me past all the tests perfectly: + let output = r#"

Hi, I've bought this very same monitor and found no calibration whatsoever. I have an ICC profile that has been set up since I've installed its driver from the LG website and it works ok. I also used http://www.lagom.nl/lcd-test/ to calibrate it. After some good tinkering I've found the following settings + the color profile from the driver gets me past all the tests perfectly:

  • Brightness 50 (still have to settle on this one, it's personal preference, it controls the backlight, not the colors)
  • Contrast 70 (which for me was the default one)
  • Picture mode Custom
  • Super resolution + Off (it looks horrible anyway)
  • Sharpness 50 (default one I think)
  • Black level High (low messes up gray colors)
  • DFC Off
  • Response Time Middle (personal preference, https://www.blurbusters.com/ show horrible overdrive with it on high)
  • Freesync doesn't matter
  • Black stabilizer 50
  • Gamma setting on 0
  • Color Temp Medium
How`s your monitor by the way? Any IPS bleed whatsoever? I either got lucky or the panel is pretty good, 0 bleed for me, just the usual IPS glow. How about the pixels? I see the pixels even at one meter away, especially on Microsoft Edge's icon for example, the blue background is just blocky, don't know why.

"#; assert_eq!(render_bullet_lists(input), output); -} \ No newline at end of file +} From ef2cc01bf7f43018dddf60d9b575b1b66934c2c1 Mon Sep 17 00:00:00 2001 From: Integral Date: Mon, 3 Feb 2025 13:51:54 +0800 Subject: [PATCH 30/57] refactor(utils): avoid redundant String conversions & use match (#347) * refactor(utils): avoid redundant String conversions & use match * ci: fix clippy lint --- src/utils.rs | 57 ++++++++++++++++++++++++---------------------------- 1 file changed, 26 insertions(+), 31 deletions(-) diff --git a/src/utils.rs b/src/utils.rs index 867f73f..21461a7 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -638,7 +638,7 @@ pub struct Preferences { pub hide_score: String, } -fn serialize_vec_with_plus(vec: &Vec, serializer: S) -> Result +fn serialize_vec_with_plus(vec: &[String], serializer: S) -> Result where S: Serializer, { @@ -915,11 +915,12 @@ pub fn setting_or_default(req: &Request, name: &str, default: String) -> S // Detect and redirect in the event of a random subreddit pub async fn catch_random(sub: &str, additional: &str) -> Result, String> { if sub == "random" || sub == "randnsfw" { - let new_sub = json(format!("/r/{sub}/about.json?raw_json=1"), false).await?["data"]["display_name"] - .as_str() - .unwrap_or_default() - .to_string(); - Ok(redirect(&format!("/r/{new_sub}{additional}"))) + Ok(redirect(&format!( + "/r/{}{additional}", + json(format!("/r/{sub}/about.json?raw_json=1"), false).await?["data"]["display_name"] + .as_str() + .unwrap_or_default() + ))) } else { Err("No redirect needed".to_string()) } @@ -1019,8 +1020,7 @@ static REDLIB_PREVIEW_TEXT_REGEX: Lazy = Lazy::new(|| Regex::new(r">(.*?) pub fn rewrite_urls(input_text: &str) -> String { let mut text1 = // Rewrite Reddit links to Redlib - REDDIT_REGEX.replace_all(input_text, r#"href="/"#) - .to_string(); + REDDIT_REGEX.replace_all(input_text, r#"href="/"#).to_string(); loop { if REDDIT_EMOJI_REGEX.find(&text1).is_none() { @@ -1042,49 +1042,44 @@ pub fn rewrite_urls(input_text: &str) -> String { } else { let formatted_url = format_url(REDDIT_PREVIEW_REGEX.find(&text1).map(|x| x.as_str()).unwrap_or_default()); - let image_url = REDLIB_PREVIEW_LINK_REGEX.find(&formatted_url).map_or("", |m| m.as_str()).to_string(); - let mut image_caption = REDLIB_PREVIEW_TEXT_REGEX.find(&formatted_url).map_or("", |m| m.as_str()).to_string(); + let image_url = REDLIB_PREVIEW_LINK_REGEX.find(&formatted_url).map_or("", |m| m.as_str()); + let mut image_caption = REDLIB_PREVIEW_TEXT_REGEX.find(&formatted_url).map_or("", |m| m.as_str()); /* As long as image_caption isn't empty remove first and last four characters of image_text to leave us with just the text in the caption without any HTML. This makes it possible to enclose it in a
later on without having stray HTML breaking it */ if !image_caption.is_empty() { - image_caption = image_caption[1..image_caption.len() - 4].to_string(); + image_caption = &image_caption[1..image_caption.len() - 4]; } // image_url contains > at the end of it, and right above this we remove image_text's front >, leaving us with just a single > between them let image_to_replace = format!("

"); - // _image_replacement needs to be in scope for the replacement at the bottom of the loop - let mut _image_replacement = String::new(); - /* We don't want to show a caption that's just the image's link, so we check if we find a Reddit preview link within the image's caption. If we don't find one we must have actual text, so we include a
block that contains it. Otherwise we don't include the
block as we don't need it. */ - if REDDIT_PREVIEW_REGEX.find(&image_caption).is_none() { + let _image_replacement = if REDDIT_PREVIEW_REGEX.find(image_caption).is_none() { // Without this " would show as \" instead. "\"" is how the quotes are formatted within image_text beforehand - image_caption = image_caption.replace("\\"", "\""); - - _image_replacement = format!("
{image_caption}
"); + format!( + "
{}
", + image_caption.replace("\\"", "\"") + ) } else { - _image_replacement = format!("
"); - } + format!("
") + }; /* In order to know if we're dealing with a normal or external preview we need to take a look at the first capture group of REDDIT_PREVIEW_REGEX if it's preview we're dealing with something that needs /preview/pre, external-preview is /preview/external-pre, and i is /img */ - let reddit_preview_regex_capture = REDDIT_PREVIEW_REGEX.captures(&text1).unwrap().get(1).map_or("", |m| m.as_str()).to_string(); - let mut _preview_type = String::new(); - if reddit_preview_regex_capture == "preview" { - _preview_type = "/preview/pre".to_string(); - } else if reddit_preview_regex_capture == "external-preview" { - _preview_type = "/preview/external-pre".to_string(); - } else { - _preview_type = "/img".to_string(); - } + let reddit_preview_regex_capture = REDDIT_PREVIEW_REGEX.captures(&text1).unwrap().get(1).map_or("", |m| m.as_str()); + + let _preview_type = match reddit_preview_regex_capture { + "preview" => "/preview/pre", + "external-preview" => "/preview/external-pre", + _ => "/img", + }; text1 = REDDIT_PREVIEW_REGEX .replace(&text1, format!("{_preview_type}$2")) .replace(&image_to_replace, &_image_replacement) - .to_string() } } } @@ -1158,7 +1153,7 @@ pub fn rewrite_emotes(media_metadata: &Value, comment: String) -> String { ); // Inside the comment replace the ID we found with the string that will embed the image - comment = comment.replace(&id, &to_replace_with).to_string(); + comment = comment.replace(&id, &to_replace_with); } } } From c7f55c146a641540c293967e53ab6ee55eac2516 Mon Sep 17 00:00:00 2001 From: Matthew Esposito Date: Mon, 3 Feb 2025 00:53:13 -0500 Subject: [PATCH 31/57] fix(clippy): minor clippy changes --- src/utils.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils.rs b/src/utils.rs index 21461a7..23db3d0 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1004,7 +1004,7 @@ static REGEX_BULLET_CONSECUTIVE_LINES: Lazy = Lazy::new(|| Regex::new(r"< pub fn render_bullet_lists(input_text: &str) -> String { // ref: https://stackoverflow.com/a/4902622 // First enclose each bullet with
  • tags - let text1 = REGEX_BULLET.replace_all(&input_text, "
    • $1
    ").to_string(); + let text1 = REGEX_BULLET.replace_all(input_text, "
    • $1
    ").to_string(); // Then remove any consecutive
    tags REGEX_BULLET_CONSECUTIVE_LINES.replace_all(&text1, "").to_string() } @@ -1344,7 +1344,7 @@ pub fn url_path_basename(path: &str) -> String { let mut url = url_result.unwrap(); url.path_segments_mut().unwrap().pop_if_empty(); - url.path_segments().unwrap().last().unwrap().to_string() + url.path_segments().unwrap().next_back().unwrap().to_string() } } From 7770c57856f2ee805372cc610d5aa71fe7df5fa2 Mon Sep 17 00:00:00 2001 From: freedit-dev Date: Mon, 3 Feb 2025 13:58:14 +0800 Subject: [PATCH 32/57] fix Code blocks err #227 (#323) * fix Code blocks https://github.com/redlib-org/redlib/issues/227 * add pulldown-cmark * add pulldown-cmark * fix Code blocks err #227 * add pre style for post codeblock * Update style.css (fix Code blocks err #227 ) --------- Co-authored-by: Matthew Esposito --- Cargo.lock | 19 +++++++++++++++++++ Cargo.toml | 1 + src/utils.rs | 10 +++++++++- static/style.css | 7 +++++++ 4 files changed, 36 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 24791b4..6578d17 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1302,6 +1302,24 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "pulldown-cmark" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f86ba2052aebccc42cbbb3ed234b8b13ce76f75c3551a303cb2bcffcff12bb14" +dependencies = [ + "bitflags", + "memchr", + "pulldown-cmark-escape", + "unicase", +] + +[[package]] +name = "pulldown-cmark-escape" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae" + [[package]] name = "quick-error" version = "1.2.3" @@ -1383,6 +1401,7 @@ dependencies = [ "once_cell", "percent-encoding", "pretty_env_logger", + "pulldown-cmark", "regex", "rinja", "route-recognizer", diff --git a/Cargo.toml b/Cargo.toml index 843b9c9..fc9074b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,6 +47,7 @@ rss = "2.0.7" arc-swap = "1.7.1" serde_json_path = "0.7.1" async-recursion = "1.1.1" +pulldown-cmark = { version = "0.12.0", features = ["simd", "html"], default-features = false } common-words-all = { version = "0.0.2", default-features = false, features = ["english", "one"] } hyper-rustls = { version = "0.24.2", features = [ "http2" ] } tegen = "0.1.4" diff --git a/src/utils.rs b/src/utils.rs index 23db3d0..0aa3159 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -743,7 +743,15 @@ pub async fn parse_post(post: &Value) -> Post { get_setting("REDLIB_PUSHSHIFT_FRONTEND").unwrap_or_else(|| String::from(crate::config::DEFAULT_PUSHSHIFT_FRONTEND)), ) } else { - rewrite_urls(&val(post, "selftext_html")) + let selftext = val(post, "selftext"); + if selftext.contains("```") { + let mut html_output = String::new(); + let parser = pulldown_cmark::Parser::new(&selftext); + pulldown_cmark::html::push_html(&mut html_output, parser); + rewrite_urls(&html_output) + } else { + rewrite_urls(&val(post, "selftext_html")) + } }; // Build a post using data parsed from Reddit post API diff --git a/static/style.css b/static/style.css index a9d893a..76b55dd 100644 --- a/static/style.css +++ b/static/style.css @@ -1199,6 +1199,13 @@ a.search_subreddit:hover { overflow-wrap: anywhere; } +.post_body pre { + background: var(--background); + overflow-x: auto; + margin: 10px 0; + padding: 10px; +} + .post_body img { max-width: 100%; display: block; From a732f181430c14b3a292b54fd372e069018ab03c Mon Sep 17 00:00:00 2001 From: Matthew Esposito Date: Mon, 3 Feb 2025 14:25:16 -0500 Subject: [PATCH 33/57] chore: remove scraper cli --- Cargo.toml | 8 --- src/scraper/main.rs | 132 -------------------------------------------- 2 files changed, 140 deletions(-) delete mode 100644 src/scraper/main.rs diff --git a/Cargo.toml b/Cargo.toml index fc9074b..759b148 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -64,11 +64,3 @@ sealed_test = "1.0.0" codegen-units = 1 lto = true strip = "symbols" - -[[bin]] -name = "redlib" -path = "src/main.rs" - -[[bin]] -name = "scraper" -path = "src/scraper/main.rs" diff --git a/src/scraper/main.rs b/src/scraper/main.rs deleted file mode 100644 index f2e48d6..0000000 --- a/src/scraper/main.rs +++ /dev/null @@ -1,132 +0,0 @@ -use std::{collections::HashMap, fmt::Display, io::Write}; - -use clap::{Parser, ValueEnum}; -use common_words_all::{get_top, Language, NgramSize}; -use redlib::utils::Post; - -#[derive(Parser)] -#[command(name = "my_cli")] -#[command(about = "A simple CLI example", long_about = None)] -struct Cli { - #[arg(short = 's', long = "sub")] - sub: String, - - #[arg(long = "sort")] - sort: SortOrder, - - #[arg(short = 'f', long = "format", value_enum)] - format: Format, - #[arg(short = 'o', long = "output")] - output: Option, -} - -#[derive(Debug, Clone, ValueEnum)] -enum SortOrder { - Hot, - Rising, - New, - Top, - Controversial, -} - -impl Display for SortOrder { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - SortOrder::Hot => write!(f, "hot"), - SortOrder::Rising => write!(f, "rising"), - SortOrder::New => write!(f, "new"), - SortOrder::Top => write!(f, "top"), - SortOrder::Controversial => write!(f, "controversial"), - } - } -} - -#[derive(Debug, Clone, ValueEnum)] -enum Format { - Json, -} - -#[tokio::main] -async fn main() { - pretty_env_logger::init(); - let cli = Cli::parse(); - let (sub, sort, format, output) = (cli.sub, cli.sort, cli.format, cli.output); - let initial = format!("/r/{sub}/{sort}.json?&raw_json=1"); - let (posts, mut after) = Post::fetch(&initial, false).await.unwrap(); - let mut hashmap = HashMap::new(); - hashmap.extend(posts.into_iter().map(|post| (post.id.clone(), post))); - loop { - print!("\r"); - let path = format!("/r/{sub}/{sort}.json?sort={sort}&t=&after={after}&raw_json=1"); - let (new_posts, new_after) = Post::fetch(&path, false).await.unwrap(); - let old_len = hashmap.len(); - // convert to hashmap and extend hashmap - let new_posts = new_posts.into_iter().map(|post| (post.id.clone(), post)).collect::>(); - let len = new_posts.len(); - hashmap.extend(new_posts); - if hashmap.len() - old_len < 3 { - break; - } - - let x = hashmap.len() - old_len; - after = new_after; - // Print number of posts fetched - print!("Fetched {len} posts (+{x})",); - std::io::stdout().flush().unwrap(); - } - println!("\n\n"); - // additionally search if final count not reached - - for word in get_top(Language::English, 10_000, NgramSize::One) { - let mut retrieved_posts_from_search = 0; - let initial = format!("/r/{sub}/search.json?q={word}&restrict_sr=on&include_over_18=on&raw_json=1&sort={sort}"); - println!("Grabbing posts with word {word}."); - let (posts, mut after) = Post::fetch(&initial, false).await.unwrap(); - hashmap.extend(posts.into_iter().map(|post| (post.id.clone(), post))); - 'search: loop { - let path = format!("/r/{sub}/search.json?q={word}&restrict_sr=on&include_over_18=on&raw_json=1&sort={sort}&after={after}"); - let (new_posts, new_after) = Post::fetch(&path, false).await.unwrap(); - if new_posts.is_empty() || new_after.is_empty() { - println!("No more posts for word {word}"); - break 'search; - } - retrieved_posts_from_search += new_posts.len(); - let old_len = hashmap.len(); - let new_posts = new_posts.into_iter().map(|post| (post.id.clone(), post)).collect::>(); - let len = new_posts.len(); - hashmap.extend(new_posts); - let delta = hashmap.len() - old_len; - after = new_after; - // Print number of posts fetched - println!("Fetched {len} posts (+{delta})",); - - if retrieved_posts_from_search > 1000 { - println!("Reached 1000 posts from search"); - break 'search; - } - } - // Need to save incrementally. atomic save + move - let tmp_file = output.clone().unwrap_or_else(|| format!("{sub}.json.tmp")); - let perm_file = output.clone().unwrap_or_else(|| format!("{sub}.json")); - write_posts(&hashmap.values().collect(), tmp_file.clone()); - // move file - std::fs::rename(tmp_file, perm_file).unwrap(); - } - - println!("\n\n"); - - println!("Size of hashmap: {}", hashmap.len()); - - let posts: Vec<&Post> = hashmap.values().collect(); - match format { - Format::Json => { - let filename: String = output.unwrap_or_else(|| format!("{sub}.json")); - write_posts(&posts, filename); - } - } -} - -fn write_posts(posts: &Vec<&Post>, filename: String) { - let json = serde_json::to_string(&posts).unwrap(); - std::fs::write(filename, json).unwrap(); -} From 85329c96a79c87b2be7477d4982746e294e2ee00 Mon Sep 17 00:00:00 2001 From: Matthew Esposito Date: Thu, 6 Feb 2025 09:02:55 -0500 Subject: [PATCH 34/57] fix: remove stray trace --- src/subreddit.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/subreddit.rs b/src/subreddit.rs index 0db4f77..f7fe01d 100644 --- a/src/subreddit.rs +++ b/src/subreddit.rs @@ -64,7 +64,6 @@ pub async fn community(req: Request) -> Result, String> { // Build Reddit API path let root = req.uri().path() == "/"; let query = req.uri().query().unwrap_or_default().to_string(); - trace!("query: {}", query); let subscribed = setting(&req, "subscriptions"); let front_page = setting(&req, "front_page"); let post_sort = req.cookie("post_sort").map_or_else(|| "hot".to_string(), |c| c.value().to_string()); From 5265ccb0332f03f01eb37ccc648d17608b148bd8 Mon Sep 17 00:00:00 2001 From: Matthew Esposito Date: Thu, 6 Feb 2025 13:03:42 -0500 Subject: [PATCH 35/57] feat: hide default feeds option (#370) --- README.md | 3 ++- src/settings.rs | 3 ++- src/subreddit.rs | 21 +++++++++++++++++++-- src/utils.rs | 27 ++++++++++++++++++++++++++- templates/info.html | 20 ++++++++++++++++++++ templates/settings.html | 5 +++++ templates/utils.html | 6 ++++-- 7 files changed, 78 insertions(+), 7 deletions(-) create mode 100644 templates/info.html diff --git a/README.md b/README.md index 6fabfcc..5aac1e8 100644 --- a/README.md +++ b/README.md @@ -440,7 +440,7 @@ Assign a default value for each user-modifiable setting by passing environment v | `WIDE` | `["on", "off"]` | `off` | | `POST_SORT` | `["hot", "new", "top", "rising", "controversial"]` | `hot` | | `COMMENT_SORT` | `["confidence", "top", "new", "controversial", "old"]` | `confidence` | -| `BLUR_SPOILER` | `["on", "off"]` | `off` | +| `BLUR_SPOILER` | `["on", "off"]` | `off` | | `SHOW_NSFW` | `["on", "off"]` | `off` | | `BLUR_NSFW` | `["on", "off"]` | `off` | | `USE_HLS` | `["on", "off"]` | `off` | @@ -452,3 +452,4 @@ Assign a default value for each user-modifiable setting by passing environment v | `HIDE_SCORE` | `["on", "off"]` | `off` | | `HIDE_SIDEBAR_AND_SUMMARY` | `["on", "off"]` | `off` | | `FIXED_NAVBAR` | `["on", "off"]` | `on` | +| `REMOVE_DEFAULT_FEEDS` | `["on", "off"]` | `off` | \ No newline at end of file diff --git a/src/settings.rs b/src/settings.rs index 34718c2..4b30da3 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -22,7 +22,7 @@ struct SettingsTemplate { // CONSTANTS -const PREFS: [&str; 18] = [ +const PREFS: [&str; 19] = [ "theme", "front_page", "layout", @@ -41,6 +41,7 @@ const PREFS: [&str; 18] = [ "hide_score", "disable_visit_reddit_confirmation", "video_quality", + "remove_default_feeds", ]; // FUNCTIONS diff --git a/src/subreddit.rs b/src/subreddit.rs index f7fe01d..d03a9dd 100644 --- a/src/subreddit.rs +++ b/src/subreddit.rs @@ -3,12 +3,13 @@ use crate::{config, utils}; // CRATES use crate::utils::{ - catch_random, error, filter_posts, format_num, format_url, get_filters, nsfw_landing, param, redirect, rewrite_urls, setting, template, val, Post, Preferences, Subreddit, + catch_random, error, filter_posts, format_num, format_url, get_filters, info, nsfw_landing, param, redirect, rewrite_urls, setting, template, val, Post, Preferences, + Subreddit, }; use crate::{client::json, server::RequestExt, server::ResponseExt}; use cookie::Cookie; use hyper::{Body, Request, Response}; -use log::{debug, trace}; +use log::debug; use rinja::Template; use chrono::DateTime; @@ -66,6 +67,7 @@ pub async fn community(req: Request) -> Result, String> { let query = req.uri().query().unwrap_or_default().to_string(); let subscribed = setting(&req, "subscriptions"); let front_page = setting(&req, "front_page"); + let remove_default_feeds = setting(&req, "remove_default_feeds") == "on"; let post_sort = req.cookie("post_sort").map_or_else(|| "hot".to_string(), |c| c.value().to_string()); let sort = req.param("sort").unwrap_or_else(|| req.param("id").unwrap_or(post_sort)); @@ -78,6 +80,21 @@ pub async fn community(req: Request) -> Result, String> { } else { front_page.clone() }); + + if (sub_name == "popular" || sub_name == "all") && remove_default_feeds { + if subscribed.is_empty() { + return info(req, "Subscribe to some subreddits! (Default feeds disabled in settings)").await; + } else { + // If there are subscribed subs, but we get here, then the problem is that front_page pref is set to something besides default. + // Tell user to go to settings and change front page to default. + return info( + req, + "You have subscribed to some subreddits, but your front page is not set to default. Visit settings and change front page to default.", + ) + .await; + } + } + let quarantined = can_access_quarantine(&req, &sub_name) || root; // Handle random subreddits diff --git a/src/utils.rs b/src/utils.rs index 0aa3159..2747449 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -551,6 +551,14 @@ pub struct ErrorTemplate { pub url: String, } +#[derive(Template)] +#[template(path = "info.html")] +pub struct InfoTemplate { + pub msg: String, + pub prefs: Preferences, + pub url: String, +} + /// Template for NSFW landing page. The landing page is displayed when a page's /// content is wholly NSFW, but a user has not enabled the option to view NSFW /// posts. @@ -636,6 +644,7 @@ pub struct Preferences { pub filters: Vec, pub hide_awards: String, pub hide_score: String, + pub remove_default_feeds: String, } fn serialize_vec_with_plus(vec: &[String], serializer: S) -> Result @@ -682,6 +691,7 @@ impl Preferences { filters: setting(req, "filters").split('+').map(String::from).filter(|s| !s.is_empty()).collect(), hide_awards: setting(req, "hide_awards"), hide_score: setting(req, "hide_score"), + remove_default_feeds: setting(req, "remove_default_feeds"), } } @@ -1265,6 +1275,20 @@ pub async fn error(req: Request, msg: &str) -> Result, Stri Ok(Response::builder().status(404).header("content-type", "text/html").body(body.into()).unwrap_or_default()) } +/// Renders a generic info landing page. +pub async fn info(req: Request, msg: &str) -> Result, String> { + let url = req.uri().to_string(); + let body = InfoTemplate { + msg: msg.to_string(), + prefs: Preferences::new(&req), + url, + } + .render() + .unwrap_or_default(); + + Ok(Response::builder().status(200).header("content-type", "text/html").body(body.into()).unwrap_or_default()) +} + /// Returns true if the config/env variable `REDLIB_SFW_ONLY` carries the /// value `on`. /// @@ -1463,10 +1487,11 @@ mod tests { filters: vec![], hide_awards: "off".to_owned(), hide_score: "off".to_owned(), + remove_default_feeds: "off".to_owned(), }; let urlencoded = serde_urlencoded::to_string(prefs).expect("Failed to serialize Prefs"); - assert_eq!(urlencoded, "theme=laserwave&front_page=default&layout=compact&wide=on&blur_spoiler=on&show_nsfw=off&blur_nsfw=on&hide_hls_notification=off&video_quality=best&hide_sidebar_and_summary=off&use_hls=on&autoplay_videos=on&fixed_navbar=on&disable_visit_reddit_confirmation=on&comment_sort=confidence&post_sort=top&subscriptions=memes%2Bmildlyinteresting&filters=&hide_awards=off&hide_score=off") + assert_eq!(urlencoded, "theme=laserwave&front_page=default&layout=compact&wide=on&blur_spoiler=on&show_nsfw=off&blur_nsfw=on&hide_hls_notification=off&video_quality=best&hide_sidebar_and_summary=off&use_hls=on&autoplay_videos=on&fixed_navbar=on&disable_visit_reddit_confirmation=on&comment_sort=confidence&post_sort=top&subscriptions=memes%2Bmildlyinteresting&filters=&hide_awards=off&hide_score=off&remove_default_feeds=off"); } } diff --git a/templates/info.html b/templates/info.html new file mode 100644 index 0000000..a14c170 --- /dev/null +++ b/templates/info.html @@ -0,0 +1,20 @@ +{% extends "base.html" %} +{% import "utils.html" as utils %} + +{% block title %}Info: {{ msg }}{% endblock %} +{% block sortstyle %}{% endblock %} + +{% block subscriptions %} + {% call utils::sub_list("") %} +{% endblock %} + +{% block search %} + {% call utils::search("".to_owned(), "") %} +{% endblock %} + +{% block content %} +
    +

    {{ msg }}

    +
    +
    +{% endblock %} diff --git a/templates/settings.html b/templates/settings.html index fef91cf..7995312 100644 --- a/templates/settings.html +++ b/templates/settings.html @@ -26,6 +26,11 @@
    Interface +
    + + + +
    +
    @@ -29,144 +29,162 @@
    - +
    - {% call utils::options(prefs.front_page, ["default", "popular", "all"], "default") %}
    - {% call utils::options(prefs.layout, ["card", "clean", "compact"], "card") %}
    - +
    Content
    - {% call utils::options(prefs.video_quality, ["best", "medium", "worst"], "best") %}
    - + {% call utils::options(prefs.post_sort, ["hot", "new", "top", "rising", "controversial"], "hot") + %}
    - + {% call utils::options(prefs.comment_sort, ["confidence", "top", "new", "controversial", "old"], + "confidence") %}
    - +
    - {% if !crate::utils::sfw_only() %} + {% if !crate::utils::sfw_only() %}
    - +
    - +
    - {% endif %} + {% endif %}
    - +
    - +
    - +
    Why? -
    Reddit videos require JavaScript (via HLS.js) to be enabled to be played with audio. Therefore, this toggle lets you either use Redlib JS-free or utilize this feature.
    +
    Reddit videos require JavaScript (via HLS.js) to be enabled + to be played with audio. Therefore, this toggle lets you either use Redlib JS-free or + utilize this feature.
    - +
    - +
    - +
    - +
    - + - +
{% if prefs.subscriptions.len() > 0 %} -
- Subscribed Feeds - {% for sub in prefs.subscriptions %} - - {% endfor %} +
+ Subscribed Feeds + {% for sub in prefs.subscriptions %} +
+ {% let feed -%} + {% if sub.starts_with("u_") -%}{% let feed = format!("u/{}", &sub[2..]) -%}{% else -%}{% let feed = + format!("r/{}", sub) -%}{% endif -%} + {{ feed }} +
+ +
+ {% endfor %} +
{% endif %} {% if !prefs.filters.is_empty() %} -
- Filtered Feeds - {% for sub in prefs.filters %} -
- {% let feed -%} - {% if sub.starts_with("u_") -%}{% let feed = format!("u/{}", &sub[2..]) -%}{% else -%}{% let feed = format!("r/{}", sub) -%}{% endif -%} - {{ feed }} -
- -
-
- {% endfor %} +
+ Filtered Feeds + {% for sub in prefs.filters %} +
+ {% let feed -%} + {% if sub.starts_with("u_") -%}{% let feed = format!("u/{}", &sub[2..]) -%}{% else -%}{% let feed = + format!("r/{}", sub) -%}{% endif -%} + {{ feed }} +
+ +
+ {% endfor %} +
{% endif %}
-

Note: settings and subscriptions are saved in browser cookies. Clearing your cookies will reset them.

+

Note: settings and subscriptions are saved in browser cookies. Clearing your cookies will reset them. +


{% match prefs.to_urlencoded() %} {% when Ok with (encoded_prefs) %} @@ -176,7 +194,24 @@

There was an error creating your restore link: {{ err }}

Please report this issue

{% endmatch %} +
+
+ + +
+ + + +
+
+ + +
+
-{% endblock %} +{% endblock %} \ No newline at end of file From ebc682da2d01f3ddf412040eb4a2614b286be730 Mon Sep 17 00:00:00 2001 From: Matthew Esposito Date: Sat, 8 Feb 2025 16:59:32 -0500 Subject: [PATCH 38/57] chore: update error page --- static/check_update.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/static/check_update.js b/static/check_update.js index b747203..4cd745b 100644 --- a/static/check_update.js +++ b/static/check_update.js @@ -33,7 +33,7 @@ async function checkInstanceUpdateStatus() { document.getElementById('update-status').innerText = statusMessage; } catch (error) { console.error('Error fetching commits:', error); - document.getElementById('update-status').innerText = '⚠️ Error checking update status.'; + document.getElementById('update-status').innerText = '⚠️ Error checking update status: ' + error; } } @@ -48,7 +48,7 @@ async function checkOtherInstances() { document.getElementById('random-instance').innerText = "Visit Random Instance"; } catch (error) { console.error('Error fetching instances:', error); - document.getElementById('update-status').innerText = '⚠️ Error checking update status.'; + document.getElementById('update-status').innerText = '⚠️ Error checking other instances: ' + error; } } From bb20190555d42df97695451ead8aff2b0be994bb Mon Sep 17 00:00:00 2001 From: Matthew Esposito Date: Sun, 9 Feb 2025 17:10:12 -0500 Subject: [PATCH 39/57] fix: control rendering behavior based on routing --- src/subreddit.rs | 3 ++- src/user.rs | 3 ++- src/utils.rs | 3 +-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/subreddit.rs b/src/subreddit.rs index d03a9dd..e6d1cca 100644 --- a/src/subreddit.rs +++ b/src/subreddit.rs @@ -8,6 +8,7 @@ use crate::utils::{ }; use crate::{client::json, server::RequestExt, server::ResponseExt}; use cookie::Cookie; +use htmlescape::decode_html; use hyper::{Body, Request, Response}; use log::debug; use rinja::Template; @@ -623,7 +624,7 @@ pub async fn rss(req: Request) -> Result, String> { title: Some(post.title.to_string()), link: Some(format_url(&utils::get_post_url(&post))), author: Some(post.author.name), - content: Some(rewrite_urls(&post.body)), + content: Some(rewrite_urls(&decode_html(&post.body).unwrap())), pub_date: Some(DateTime::from_timestamp(post.created_ts as i64, 0).unwrap_or_default().to_rfc2822()), description: Some(format!( "Comments", diff --git a/src/user.rs b/src/user.rs index 818f368..592389d 100644 --- a/src/user.rs +++ b/src/user.rs @@ -6,6 +6,7 @@ use crate::server::RequestExt; use crate::utils::{error, filter_posts, format_url, get_filters, nsfw_landing, param, setting, template, Post, Preferences, User}; use crate::{config, utils}; use chrono::DateTime; +use htmlescape::decode_html; use hyper::{Body, Request, Response}; use rinja::Template; use time::{macros::format_description, OffsetDateTime}; @@ -167,7 +168,7 @@ pub async fn rss(req: Request) -> Result, String> { link: Some(format_url(&utils::get_post_url(&post))), author: Some(post.author.name), pub_date: Some(DateTime::from_timestamp(post.created_ts as i64, 0).unwrap_or_default().to_rfc2822()), - content: Some(rewrite_urls(&post.body)), + content: Some(rewrite_urls(&decode_html(&post.body).unwrap())), ..Default::default() }) .collect::>(), diff --git a/src/utils.rs b/src/utils.rs index c4ab679..f5046cb 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -7,7 +7,6 @@ use crate::config::{self, get_setting}; // use crate::{client::json, server::RequestExt}; use cookie::Cookie; -use htmlescape::decode_html; use hyper::{Body, Request, Response}; use libflate::deflate::{Decoder, Encoder}; use log::error; @@ -388,7 +387,7 @@ impl Post { let awards = Awards::parse(&data["all_awardings"]); // selftext_html is set for text posts when browsing. - let mut body = rewrite_urls(&decode_html(&val(post, "selftext_html")).unwrap()); + let mut body = rewrite_urls(&val(post, "selftext_html")); if body.is_empty() { body = rewrite_urls(&val(post, "body_html")); } From c9dbd7a3ccc9fb0bf8b29c7234b5ab344487026b Mon Sep 17 00:00:00 2001 From: Matthew Esposito Date: Thu, 13 Feb 2025 21:42:09 -0500 Subject: [PATCH 40/57] fix: debug string --- src/subreddit.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/subreddit.rs b/src/subreddit.rs index e6d1cca..1609ead 100644 --- a/src/subreddit.rs +++ b/src/subreddit.rs @@ -143,7 +143,6 @@ pub async fn community(req: Request) -> Result, String> { } let path = format!("/r/{}/{sort}.json?{}{params}", sub_name.replace('+', "%2B"), req.uri().query().unwrap_or_default()); - debug!("Path: {}", path); let url = String::from(req.uri().path_and_query().map_or("", |val| val.as_str())); let redirect_url = url[1..].replace('?', "%3F").replace('&', "%26").replace('+', "%2B"); let filters = get_filters(&req); From 9afe886c2c120bd5b9c24dd33341a3a70792b25b Mon Sep 17 00:00:00 2001 From: Matthew Esposito Date: Thu, 13 Feb 2025 21:42:29 -0500 Subject: [PATCH 41/57] chore(clippy) --- src/subreddit.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/subreddit.rs b/src/subreddit.rs index 1609ead..f84cca3 100644 --- a/src/subreddit.rs +++ b/src/subreddit.rs @@ -10,7 +10,6 @@ use crate::{client::json, server::RequestExt, server::ResponseExt}; use cookie::Cookie; use htmlescape::decode_html; use hyper::{Body, Request, Response}; -use log::debug; use rinja::Template; use chrono::DateTime; From efcf2fc24c455acdf2ddc4adc8d20606883aa118 Mon Sep 17 00:00:00 2001 From: Matthew Esposito Date: Mon, 17 Feb 2025 22:41:01 -0500 Subject: [PATCH 42/57] feat: display contexted title if link is single-thread (#383) --- templates/post.html | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/templates/post.html b/templates/post.html index 17ad456..adbe6d6 100644 --- a/templates/post.html +++ b/templates/post.html @@ -1,7 +1,13 @@ {% extends "base.html" %} {% import "utils.html" as utils %} -{% block title %}{{ post.title }} - r/{{ post.community }}{% endblock %} +{% block title %} + {% if single_thread %} + {{ comments[0].author.name }} comments on {{ post.title }} - r/{{ post.community }} + {% else %} + {{ post.title }} - r/{{ post.community }} + {% endif %} +{% endblock %} {% block search %} {% call utils::search(["/r/", post.community.as_str()].concat(), "") %} From 35688e4af7397086df3ada1721ceb9cd6c24ba7e Mon Sep 17 00:00:00 2001 From: Matthew Esposito Date: Wed, 26 Feb 2025 13:17:08 -0500 Subject: [PATCH 43/57] fix: update default setting for removing default feeds --- app.json | 3 +++ src/config.rs | 5 +++++ src/instance_info.rs | 3 +++ 3 files changed, 11 insertions(+) diff --git a/app.json b/app.json index c05b26d..4af7cfe 100644 --- a/app.json +++ b/app.json @@ -76,6 +76,9 @@ }, "REDLIB_FULL_URL": { "required": false + }, + "REDLIB_DEFAULT_REMOVE_DEFAULT_FEEDS": { + "required": false } } } diff --git a/src/config.rs b/src/config.rs index 034afc7..7b1c95c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -109,6 +109,9 @@ pub struct Config { #[serde(rename = "REDLIB_FULL_URL")] pub(crate) full_url: Option, + + #[serde(rename = "REDLIB_DEFAULT_REMOVE_DEFAULT_FEEDS")] + pub(crate) default_remove_default_feeds: Option, } impl Config { @@ -156,6 +159,7 @@ impl Config { pushshift: parse("REDLIB_PUSHSHIFT_FRONTEND"), enable_rss: parse("REDLIB_ENABLE_RSS"), full_url: parse("REDLIB_FULL_URL"), + default_remove_default_feeds: parse("REDLIB_DEFAULT_REMOVE_DEFAULT_FEEDS"), } } } @@ -185,6 +189,7 @@ fn get_setting_from_config(name: &str, config: &Config) -> Option { "REDLIB_PUSHSHIFT_FRONTEND" => config.pushshift.clone(), "REDLIB_ENABLE_RSS" => config.enable_rss.clone(), "REDLIB_FULL_URL" => config.full_url.clone(), + "REDLIB_DEFAULT_REMOVE_DEFAULT_FEEDS" => config.default_remove_default_feeds.clone(), _ => None, } } diff --git a/src/instance_info.rs b/src/instance_info.rs index 5f82ce6..a573953 100644 --- a/src/instance_info.rs +++ b/src/instance_info.rs @@ -128,6 +128,7 @@ impl InstanceInfo { ["Pushshift frontend", &convert(&self.config.pushshift)], ["RSS enabled", &convert(&self.config.enable_rss)], ["Full URL", &convert(&self.config.full_url)], + ["Remove default feeds", &convert(&self.config.default_remove_default_feeds)], //TODO: fallback to crate::config::DEFAULT_PUSHSHIFT_FRONTEND ]) .with_header_row(["Settings"]), @@ -169,6 +170,7 @@ impl InstanceInfo { Pushshift frontend: {:?}\n RSS enabled: {:?}\n Full URL: {:?}\n + Remove default feeds: {:?}\n Config:\n Banner: {:?}\n Hide awards: {:?}\n @@ -195,6 +197,7 @@ impl InstanceInfo { self.config.sfw_only, self.config.enable_rss, self.config.full_url, + self.config.default_remove_default_feeds, self.config.pushshift, self.config.banner, self.config.default_hide_awards, From f3ca7bb7d1f03d4eb1e19be5f1bb1b66b4c25f89 Mon Sep 17 00:00:00 2001 From: Matthew Esposito Date: Thu, 27 Feb 2025 14:35:09 -0500 Subject: [PATCH 44/57] feat: allow for case-insensitive search redirects (fix #389) --- src/search.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/search.rs b/src/search.rs index 5962c2e..bf3e65b 100644 --- a/src/search.rs +++ b/src/search.rs @@ -71,11 +71,11 @@ pub async fn find(req: Request) -> Result, String> { return Ok(redirect("/")); } - if query.starts_with("r/") || query.starts_with("user/") { + if query.starts_with("r/") || query.starts_with("R/") || query.starts_with("user/") { return Ok(redirect(&format!("/{query}"))); } - if query.starts_with("u/") { + if query.starts_with("u/") || query.starts_with("U/") { return Ok(redirect(&format!("/user{}", &query[1..]))); } From d097495a4182d0c9ba52ad9833171b2cab6d1a22 Mon Sep 17 00:00:00 2001 From: Matthew Esposito Date: Thu, 27 Feb 2025 20:10:01 -0500 Subject: [PATCH 45/57] fix: handle case insensitivity for subs --- src/search.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/search.rs b/src/search.rs index bf3e65b..1b5d1a9 100644 --- a/src/search.rs +++ b/src/search.rs @@ -71,9 +71,13 @@ pub async fn find(req: Request) -> Result, String> { return Ok(redirect("/")); } - if query.starts_with("r/") || query.starts_with("R/") || query.starts_with("user/") { + if query.starts_with("r/") || query.starts_with("user/") { return Ok(redirect(&format!("/{query}"))); } + + if query.starts_with("R/") { + return Ok(redirect(&format!("/r{}", &query[1..]))); + } if query.starts_with("u/") || query.starts_with("U/") { return Ok(redirect(&format!("/user{}", &query[1..]))); From 357e7c2e096c1aa3bb871e42860dfd3be62e0bfb Mon Sep 17 00:00:00 2001 From: Matthew Esposito Date: Sat, 1 Mar 2025 12:35:43 -0500 Subject: [PATCH 46/57] chore: remove "official" instance --- README.md | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 5aac1e8..fcae126 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ --- -**10-second pitch:** Redlib is a private front-end like [Invidious](https://github.com/iv-org/invidious) but for Reddit. Browse the coldest takes of [r/unpopularopinion](https://redlib.matthew.science/r/unpopularopinion) without being [tracked](#reddit). +**10-second pitch:** Redlib is a private front-end like [Invidious](https://github.com/iv-org/invidious) but for Reddit. Browse the coldest takes of [r/unpopularopinion](https://farside.link/redlib/r/unpopularopinion) without being [tracked](#reddit). - 🚀 Fast: written in Rust for blazing-fast speeds and memory safety - ☁️ Light: no JavaScript, no ads, no tracking, no bloat @@ -30,7 +30,6 @@ - [Reddit](#reddit) - [Redlib](#redlib-1) - [Server](#server) - - [Official instance (redlib.matthew.science)](#official-instance-redlibmatthewscience) 5. [Deployment](#deployment) - [Docker](#docker) - [Docker Compose](#docker-compose) @@ -159,17 +158,7 @@ For transparency, I hope to describe all the ways Redlib handles user privacy. - **Logging:** In production (when running the binary, hosting with docker, or using the official instances), Redlib logs nothing. When debugging (running from source without `--release`), Redlib logs post IDs fetched to aid with troubleshooting. -- **Cookies:** Redlib uses optional cookies to store any configured settings in [the settings menu](https://redlib.matthew.science/settings). These are not cross-site cookies and the cookies hold no personal data. - -#### Official instance (redlib.matthew.science) - -The official instance is hosted at https://redlib.matthew.science. - -- **Server:** The official instance runs a production binary, and thus logs nothing. - -- **DNS:** The domain for the official instance uses Cloudflare as the DNS resolver. However, this site is not proxied through Cloudflare, and thus Cloudflare doesn't have access to user traffic. - -- **Hosting:** The official instance is hosted on [Replit](https://replit.com/), which monitors usage to prevent abuse. I can understand if this invalidates certain users' threat models, and therefore, self-hosting, using unofficial instances, and browsing through Tor are welcomed. +- **Cookies:** Redlib uses optional cookies to store any configured settings in the settings menu. These are not cross-site cookies and the cookies hold no personal data. --- From 526c0d0797859d8f238036010c75b9b8a835a489 Mon Sep 17 00:00:00 2001 From: Matthew Esposito Date: Fri, 7 Mar 2025 17:08:27 -0500 Subject: [PATCH 47/57] fix: opensearch.xml route --- src/main.rs | 12 ++++++++++++ static/opensearch.xml | 6 +++--- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/main.rs b/src/main.rs index bcfcd51..e1b010d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -64,6 +64,17 @@ async fn font() -> Result, String> { ) } +async fn opensearch() -> Result, String> { + Ok( + Response::builder() + .status(200) + .header("content-type", "application/opensearchdescription+xml") + .header("Cache-Control", "public, max-age=1209600, s-maxage=86400") + .body(include_bytes!("../static/opensearch.xml").as_ref().into()) + .unwrap_or_default(), + ) +} + async fn resource(body: &str, content_type: &str, cache: bool) -> Result, String> { let mut res = Response::builder() .status(200) @@ -234,6 +245,7 @@ async fn main() { app.at("/Inter.var.woff2").get(|_| font().boxed()); app.at("/touch-icon-iphone.png").get(|_| iphone_logo().boxed()); app.at("/apple-touch-icon.png").get(|_| iphone_logo().boxed()); + app.at("/opensearch.xml").get(|_| opensearch().boxed()); app .at("/playHLSVideo.js") .get(|_| resource(include_str!("../static/playHLSVideo.js"), "text/javascript", false).boxed()); diff --git a/static/opensearch.xml b/static/opensearch.xml index 4bbef00..5864ef7 100644 --- a/static/opensearch.xml +++ b/static/opensearch.xml @@ -3,9 +3,9 @@ Search Redlib Search for whatever you want on Redlib, awesome Reddit frontend UTF-8 - /favicon.ico - + https://localhost:8080/favicon.ico + - /search + https://localhost:8080/search From c9e6ffd33cec460795f8fef27e00a92b25794429 Mon Sep 17 00:00:00 2001 From: Matthew Esposito Date: Fri, 7 Mar 2025 17:08:58 -0500 Subject: [PATCH 48/57] clippy fix --- src/search.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/search.rs b/src/search.rs index 1b5d1a9..88dcfdd 100644 --- a/src/search.rs +++ b/src/search.rs @@ -74,7 +74,7 @@ pub async fn find(req: Request) -> Result, String> { if query.starts_with("r/") || query.starts_with("user/") { return Ok(redirect(&format!("/{query}"))); } - + if query.starts_with("R/") { return Ok(redirect(&format!("/r{}", &query[1..]))); } From 3d85df5044d062d353d8d0bee4da39a733db61c9 Mon Sep 17 00:00:00 2001 From: Matthew Esposito Date: Fri, 7 Mar 2025 17:10:21 -0500 Subject: [PATCH 49/57] chore(deps): resolve dependabot, other cargo updates --- Cargo.lock | 467 +++++++++++++++++++++++++++++------------------------ 1 file changed, 258 insertions(+), 209 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a29b750..d03edad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -61,9 +61,9 @@ dependencies = [ [[package]] name = "allocator-api2" -version = "0.2.20" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45862d1c77f2228b9e10bc609d5bc203d86ebc9b87ad8d5d5167a6c9abf739d9" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "anstyle" @@ -90,9 +90,9 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.83" +version = "0.1.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" +checksum = "d556ec1359574147ec0c4fc5eb525f3f23263a592b1a9c07e0a75b427de55c97" dependencies = [ "proc-macro2", "quote", @@ -101,9 +101,9 @@ dependencies = [ [[package]] name = "atom_syndication" -version = "0.12.5" +version = "0.12.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ee79fb83c725eae67b55218870813d2fc39fd85e4f1583848ef9f4f823cfe7c" +checksum = "d2f68d23e2cb4fd958c705b91a6b4c80ceeaf27a9e11651272a8389d5ce1a4a3" dependencies = [ "chrono", "derive_builder", @@ -162,9 +162,9 @@ dependencies = [ [[package]] name = "bitflags" -version = "2.6.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" [[package]] name = "block-buffer" @@ -188,9 +188,9 @@ dependencies = [ [[package]] name = "brotli-decompressor" -version = "4.0.1" +version = "4.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a45bd2e4095a8b518033b128020dd4a55aab1c0a381ba4404a472630f4bc362" +checksum = "74fa05ad7d803d413eb8380983b092cbbaf9a85f151b871360e7b00cd7060b37" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -198,9 +198,9 @@ dependencies = [ [[package]] name = "bstr" -version = "1.11.0" +version = "1.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a68f1f47cdf0ec8ee4b941b2eee2a80cb796db73118c0dd09ac63fbe405be22" +checksum = "531a9155a481e2ee699d4f98f43c0ca4ff8ee1bfd55c31e9e98fb29d2b176fe0" dependencies = [ "memchr", "serde", @@ -208,15 +208,15 @@ dependencies = [ [[package]] name = "build_html" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225eb82ce9e70dcc0cfa6e404d0f353326b6e163bf500ec4711cec317d11935c" +checksum = "01b01f54cbdd56298a506b086691594ded3b68dcbc9437adc87c616a35e7fc89" [[package]] name = "bumpalo" -version = "3.16.0" +version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" [[package]] name = "byteorder" @@ -226,9 +226,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.8.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "cached" @@ -243,7 +243,7 @@ dependencies = [ "futures", "hashbrown 0.14.5", "once_cell", - "thiserror", + "thiserror 1.0.69", "tokio", "web-time", ] @@ -268,9 +268,9 @@ checksum = "ade8366b8bd5ba243f0a58f036cc0ca8a2f069cff1a2351ef1cac6b083e16fc0" [[package]] name = "cc" -version = "1.2.1" +version = "1.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd9de9f2205d5ef3fd67e685b0df337994ddd4495e2a28d185500d0e1edfea47" +checksum = "be714c154be609ec7f5dad223a33bf1482fff90472de28f7362806e6d4832b8c" dependencies = [ "shlex", ] @@ -283,18 +283,18 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.39" +version = "0.4.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" +checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c" dependencies = [ "num-traits", ] [[package]] name = "clap" -version = "4.5.21" +version = "4.5.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb3b4b9e5a7c7514dfa52869339ee98b3156b0bfb4e8a77c4ff4babb64b1604f" +checksum = "027bb0d98429ae334a8698531da7077bdf906419543a35a55c2cb1b66437d767" dependencies = [ "clap_builder", "clap_derive", @@ -302,9 +302,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.21" +version = "4.5.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b17a95aa67cc7b5ebd32aa5370189aa0d79069ef1c64ce893bd30fb24bff20ec" +checksum = "5589e0cba072e0f3d23791efac0fd8627b49c829c196a492e88168e6a669d863" dependencies = [ "anstyle", "clap_lex", @@ -312,9 +312,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.18" +version = "4.5.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" +checksum = "bf4ced95c6f4a675af3da73304b9ac4ed991640c36374e4b46795c49e17cf1ed" dependencies = [ "heck", "proc-macro2", @@ -324,9 +324,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afb84c814227b90d6895e01398aee0d8033c00e7466aca416fb6a8e0eb19d8a7" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" [[package]] name = "cookie" @@ -365,9 +365,9 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.2.15" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ca741a962e1b0bff6d724a1a0958b686406e853bb14061f218562e1896f95e6" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" dependencies = [ "libc", ] @@ -532,25 +532,25 @@ dependencies = [ [[package]] name = "equivalent" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.9" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "fastrand" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "486f806e73c5707928240ddc295403b1b93c96a02038563881c4a2fd84b81ac4" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "fnv" @@ -611,9 +611,9 @@ checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-lite" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cef40d21ae2c515b51041df9ed313ed21e572df340ea58a922a0aefe7e8891a1" +checksum = "f5edaec856126859abb19ed65f39e90fea3a9574b9707f13539acf4abf7eb532" dependencies = [ "fastrand", "futures-core", @@ -665,7 +665,19 @@ checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.13.3+wasi-0.2.2", + "windows-targets", ] [[package]] @@ -676,9 +688,9 @@ checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "globset" -version = "0.4.15" +version = "0.4.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15f1ce686646e7f1e19bf7d5533fe443a45dbfb990e00629110797578b42fb19" +checksum = "54a1028dfc5f5df5da8a56a73e6c153c9a9708ec57232470703592a3f18e49f5" dependencies = [ "aho-corasick", "bstr", @@ -718,9 +730,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.1" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" [[package]] name = "heck" @@ -730,15 +742,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" -version = "0.3.9" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" - -[[package]] -name = "hermit-abi" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" +checksum = "fbd780fe5cc30f81464441920d82ac8740e2e46b29a6fad543ddd075229ce37e" [[package]] name = "htmlescape" @@ -770,9 +776,9 @@ dependencies = [ [[package]] name = "httparse" -version = "1.9.5" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "httpdate" @@ -788,9 +794,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.31" +version = "0.14.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c08302e8fa335b151b788c775ff56e7a03ae64ff85c548ee820fecb70356e85" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" dependencies = [ "bytes", "futures-channel", @@ -973,51 +979,55 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.6.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" +checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" dependencies = [ "equivalent", - "hashbrown 0.15.1", + "hashbrown 0.15.2", ] [[package]] name = "inventory" -version = "0.3.15" +version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f958d3d68f4167080a18141e10381e7634563984a537f2a49a30fd8e53ac5767" +checksum = "ab08d7cd2c5897f2c949e5383ea7c7db03fb19130ffcfbf7eda795137ae3cb83" +dependencies = [ + "rustversion", +] [[package]] name = "is-terminal" -version = "0.4.13" +version = "0.4.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "261f68e344040fbd0edea105bef17c66edf46f984ddb1115b775ce31be948f4b" +checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" dependencies = [ - "hermit-abi 0.4.0", + "hermit-abi", "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "itoa" -version = "1.0.11" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "js-sys" -version = "0.3.72" +version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" dependencies = [ + "once_cell", "wasm-bindgen", ] [[package]] name = "libc" -version = "0.2.164" +version = "0.2.170" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "433bfe06b8c75da9b2e3fbea6e5329ff87748f0b144ef75306e674c3f6f7c13f" +checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828" [[package]] name = "libflate" @@ -1045,9 +1055,9 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.4.14" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +checksum = "6db9c683daf087dc577b7506e9695b3d556a9f3849903fa28186283afd6809e9" [[package]] name = "lipsum" @@ -1061,9 +1071,9 @@ dependencies = [ [[package]] name = "litemap" -version = "0.7.3" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704" +checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" [[package]] name = "lock_api" @@ -1077,9 +1087,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.22" +version = "0.4.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e" [[package]] name = "memchr" @@ -1111,22 +1121,21 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.8.0" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5" dependencies = [ "adler2", ] [[package]] name = "mio" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ - "hermit-abi 0.3.9", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.52.0", ] @@ -1172,24 +1181,24 @@ dependencies = [ [[package]] name = "object" -version = "0.36.5" +version = "0.36.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" dependencies = [ "memchr", ] [[package]] name = "once_cell" -version = "1.20.2" +version = "1.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" [[package]] name = "openssl-probe" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "parking" @@ -1228,9 +1237,9 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pin-project-lite" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] name = "pin-utils" @@ -1265,9 +1274,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.89" +version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" +checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" dependencies = [ "unicode-ident", ] @@ -1298,9 +1307,9 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" [[package]] name = "quick-xml" -version = "0.37.1" +version = "0.37.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f22f29bdff3987b4d8632ef95fd6424ec7e4e0a57e2f4fc63e489e75357f6a03" +checksum = "165859e9e55f79d67b96c5d96f4e88b6f2695a1972849c15a6a3f5c59fc2c003" dependencies = [ "encoding_rs", "memchr", @@ -1308,9 +1317,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.37" +version = "1.0.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +checksum = "c1f1914ce909e1658d9907913b4b91947430c7d9be598b15a1912935b8c04801" dependencies = [ "proc-macro2", ] @@ -1342,7 +1351,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.15", ] [[package]] @@ -1395,9 +1404,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.7" +version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" +checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1" dependencies = [ "bitflags", ] @@ -1453,15 +1462,14 @@ dependencies = [ [[package]] name = "ring" -version = "0.17.8" +version = "0.17.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +checksum = "70ac5d832aa16abd7d1def883a8545280c20a60f523a370aa3a9617c2b8550ee" dependencies = [ "cc", "cfg-if", - "getrandom", + "getrandom 0.2.15", "libc", - "spin", "untrusted", "windows-sys 0.52.0", ] @@ -1516,9 +1524,9 @@ checksum = "afab94fb28594581f62d981211a9a4d53cc8130bbcbbb89a0440d9b8e81a7746" [[package]] name = "rss" -version = "2.0.10" +version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "554a62b3dd5450fcbb0435b3db809f9dd3c6e9f5726172408f7ad3b57ed59057" +checksum = "b2107738f003660f0a91f56fd3e3bd3ab5d918b2ddaf1e1ec2136fb1c46f71bf" dependencies = [ "atom_syndication", "derive_builder", @@ -1528,9 +1536,9 @@ dependencies = [ [[package]] name = "rust-embed" -version = "8.5.0" +version = "8.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa66af4a4fdd5e7ebc276f115e895611a34739a9c1c01028383d612d550953c0" +checksum = "0b3aba5104622db5c9fc61098de54708feb732e7763d7faa2fa625899f00bf6f" dependencies = [ "rust-embed-impl", "rust-embed-utils", @@ -1539,9 +1547,9 @@ dependencies = [ [[package]] name = "rust-embed-impl" -version = "8.5.0" +version = "8.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6125dbc8867951125eec87294137f4e9c2c96566e61bf72c45095a7c77761478" +checksum = "1f198c73be048d2c5aa8e12f7960ad08443e56fd39cc26336719fdb4ea0ebaae" dependencies = [ "proc-macro2", "quote", @@ -1552,9 +1560,9 @@ dependencies = [ [[package]] name = "rust-embed-utils" -version = "8.5.0" +version = "8.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e5347777e9aacb56039b0e1f28785929a8a3b709e87482e7442c72e7c12529d" +checksum = "5a2fcdc9f40c8dc2922842ca9add611ad19f332227fc651d015881ad1552bd9a" dependencies = [ "globset", "sha2", @@ -1569,21 +1577,21 @@ checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustc-hash" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" [[package]] name = "rustix" -version = "0.38.41" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7f649912bc1495e167a6edee79151c84b1bad49748cb4f1f1167f459f6224f6" +checksum = "dade4812df5c384711475be5fcd8c162555352945401aed22a35bffeab61f657" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1629,6 +1637,12 @@ dependencies = [ "untrusted", ] +[[package]] +name = "rustversion" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" + [[package]] name = "rusty-forkfork" version = "0.4.0" @@ -1643,9 +1657,9 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.18" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "same-file" @@ -1718,9 +1732,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.12.1" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa39c7303dc58b5543c94d22c1766b0d31f2ee58306363ea622b10bbc075eaa2" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" dependencies = [ "core-foundation-sys", "libc", @@ -1728,18 +1742,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.215" +version = "1.0.218" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" +checksum = "e8dfc9d19bdbf6d17e22319da49161d5d0108e4188e8b680aef6299eed22df60" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.215" +version = "1.0.218" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" +checksum = "f09503e191f4e797cb8aac08e9a4a4695c5edf6a2e70e376d961ddd5c969f82b" dependencies = [ "proc-macro2", "quote", @@ -1748,9 +1762,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.133" +version = "1.0.140" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" dependencies = [ "itoa", "memchr", @@ -1760,9 +1774,9 @@ dependencies = [ [[package]] name = "serde_json_path" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e176fbf9bd62f75c2d8be33207fa13af2f800a506635e89759e46f934c520f4d" +checksum = "b992cea3194eea663ba99a042d61cea4bd1872da37021af56f6a37e0359b9d33" dependencies = [ "inventory", "nom", @@ -1771,26 +1785,26 @@ dependencies = [ "serde_json", "serde_json_path_core", "serde_json_path_macros", - "thiserror", + "thiserror 2.0.12", ] [[package]] name = "serde_json_path_core" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea3bfd54a421bec8328aefede43ac9f18c8c7ded3b2afc8addd44b4813d99fd0" +checksum = "dde67d8dfe7d4967b5a95e247d4148368ddd1e753e500adb34b3ffe40c6bc1bc" dependencies = [ "inventory", "serde", "serde_json", - "thiserror", + "thiserror 2.0.12", ] [[package]] name = "serde_json_path_macros" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee05bac728cc5232af5c23896b34fbdd17cf0bb0c113440588aeeb1b57c6ba1f" +checksum = "517acfa7f77ddaf5c43d5f119c44a683774e130b4247b7d3210f8924506cfac8" dependencies = [ "inventory", "serde_json_path_core", @@ -1879,26 +1893,20 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.13.2" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" [[package]] name = "socket2" -version = "0.5.7" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" dependencies = [ "libc", "windows-sys 0.52.0", ] -[[package]] -name = "spin" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" - [[package]] name = "stable_deref_trait" version = "1.2.0" @@ -1913,9 +1921,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "2.0.87" +version = "2.0.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" +checksum = "e02e925281e18ffd9d640e234264753c43edc62d64b2d4cf898f1bc5e75f3fc2" dependencies = [ "proc-macro2", "quote", @@ -1944,12 +1952,13 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.14.0" +version = "3.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c" +checksum = "2c317e0a526ee6120d8dabad239c8dadca62b24b6f168914bbbc8e2fb1f0e567" dependencies = [ "cfg-if", "fastrand", + "getrandom 0.3.1", "once_cell", "rustix", "windows-sys 0.59.0", @@ -1970,7 +1979,16 @@ version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl 2.0.12", ] [[package]] @@ -1985,10 +2003,21 @@ dependencies = [ ] [[package]] -name = "time" -version = "0.3.36" +name = "thiserror-impl" +version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.3.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dad298b01a40a23aac4580b67e3dbedb7cc8402f3592d7f49469de2ea4aecdd8" dependencies = [ "deranged", "itoa", @@ -2003,15 +2032,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" +checksum = "765c97a5b985b7c11d7bc27fa927dc4fe6af3a6dfb021d28deb60d3bf51e76ef" [[package]] name = "time-macros" -version = "0.2.18" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +checksum = "e8093bc3e81c3bc5f7879de09619d06c9a5a5e45ca44dfeeb7225bae38005c5c" dependencies = [ "num-conv", "time-core", @@ -2029,9 +2058,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.41.1" +version = "1.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cfb5bee7a6a52939ca9224d6ac897bb669134078daa8735560897f69de4d33" +checksum = "9975ea0f48b5aa3972bf2d888c238182458437cc2a19374b81b25cdf1023fb3a" dependencies = [ "backtrace", "bytes", @@ -2047,9 +2076,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", @@ -2068,9 +2097,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.12" +version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" +checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" dependencies = [ "bytes", "futures-core", @@ -2081,9 +2110,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.19" +version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" +checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148" dependencies = [ "serde", "serde_spanned", @@ -2102,9 +2131,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.22" +version = "0.22.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" +checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" dependencies = [ "indexmap", "serde", @@ -2121,9 +2150,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.40" +version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ "pin-project-lite", "tracing-core", @@ -2131,9 +2160,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.32" +version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" dependencies = [ "once_cell", ] @@ -2146,21 +2175,21 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "typenum" -version = "1.17.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" [[package]] name = "unicase" -version = "2.8.0" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e51b68083f157f853b6379db119d1c1be0e6e4dec98101079dec41f6f5cf6df" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" [[package]] name = "unicode-ident" -version = "1.0.13" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" [[package]] name = "unsafe-libyaml" @@ -2176,9 +2205,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.3" +version = "2.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d157f1b96d14500ffdc1f10ba712e780825526c03d9a49b4d0324b0d9113ada" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" dependencies = [ "form_urlencoded", "idna", @@ -2199,11 +2228,11 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.11.0" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" +checksum = "e0f540e3240398cce6128b64ba83fdbdd86129c16a3aa1a3a252efd66eb3d587" dependencies = [ - "getrandom", + "getrandom 0.3.1", ] [[package]] @@ -2214,9 +2243,9 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "wait-timeout" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" dependencies = [ "libc", ] @@ -2247,10 +2276,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] -name = "wasm-bindgen" -version = "0.2.95" +name = "wasi" +version = "0.13.3+wasi-0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" +checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" dependencies = [ "cfg-if", "once_cell", @@ -2259,13 +2297,12 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.95" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" dependencies = [ "bumpalo", "log", - "once_cell", "proc-macro2", "quote", "syn", @@ -2274,9 +2311,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.95" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2284,9 +2321,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.95" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", @@ -2297,9 +2334,12 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.95" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] [[package]] name = "web-time" @@ -2404,13 +2444,22 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.6.20" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" +checksum = "0e7f4ea97f6f78012141bcdb6a216b2609f0979ada50b20ca5b52dde2eac2bb1" dependencies = [ "memchr", ] +[[package]] +name = "wit-bindgen-rt" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" +dependencies = [ + "bitflags", +] + [[package]] name = "write16" version = "1.0.0" @@ -2425,9 +2474,9 @@ checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" [[package]] name = "yoke" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" dependencies = [ "serde", "stable_deref_trait", @@ -2437,9 +2486,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" dependencies = [ "proc-macro2", "quote", @@ -2470,18 +2519,18 @@ dependencies = [ [[package]] name = "zerofrom" -version = "0.1.4" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.4" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", From 15147cea8e42f6569a11603d661d71122f6a02dc Mon Sep 17 00:00:00 2001 From: Matthew Esposito Date: Wed, 19 Mar 2025 22:58:51 -0400 Subject: [PATCH 50/57] fix: add resource limits on encoded prefs route --- src/settings.rs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/settings.rs b/src/settings.rs index 6649f69..2efbbba 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -11,6 +11,7 @@ use futures_lite::StreamExt; use hyper::{Body, Request, Response}; use rinja::Template; use time::{Duration, OffsetDateTime}; +use tokio::time::timeout; use url::form_urlencoded; // STRUCTS @@ -269,6 +270,10 @@ pub async fn encoded_restore(req: Request) -> Result, Strin .await .map_err(|e| format!("Failed to get bytes from request body: {}", e))?; + if body.len() > 1024 * 1024 { + return Err("Request body too large".to_string()); + } + let encoded_prefs = form_urlencoded::parse(&body) .find(|(key, _)| key == "encoded_prefs") .map(|(_, value)| value) @@ -276,9 +281,15 @@ pub async fn encoded_restore(req: Request) -> Result, Strin let bytes = base2048::decode(&encoded_prefs).ok_or_else(|| "Failed to decode base2048 encoded preferences".to_string())?; - let out = deflate_decompress(bytes)?; + let out = timeout(std::time::Duration::from_secs(1), async { deflate_decompress(bytes) }) + .await + .map_err(|e| format!("Failed to decompress bytes: {}", e))??; + + let mut prefs: Preferences = timeout(std::time::Duration::from_secs(1), async { bincode::deserialize(&out) }) + .await + .map_err(|e| format!("Failed to deserialize preferences: {}", e))? + .map_err(|e| format!("Failed to deserialize bytes into Preferences struct: {}", e))?; - let mut prefs: Preferences = bincode::deserialize(&out).map_err(|e| format!("Failed to deserialize bytes into Preferences struct: {}", e))?; prefs.available_themes = vec![]; let url = format!("/settings/restore/?{}", prefs.to_urlencoded()?); From cbc3e49923cbca61243bfc1face6f6a48bb62632 Mon Sep 17 00:00:00 2001 From: Matthew Esposito Date: Wed, 19 Mar 2025 23:05:28 -0400 Subject: [PATCH 51/57] v0.36.0 --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d03edad..6722b4d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1356,7 +1356,7 @@ dependencies = [ [[package]] name = "redlib" -version = "0.35.1" +version = "0.36.0" dependencies = [ "arc-swap", "async-recursion", diff --git a/Cargo.toml b/Cargo.toml index c7b6d4a..bb76d0a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ name = "redlib" description = " Alternative private front-end to Reddit" license = "AGPL-3.0-only" repository = "https://github.com/redlib-org/redlib" -version = "0.35.1" +version = "0.36.0" authors = [ "Matthew Esposito ", "spikecodes <19519553+spikecodes@users.noreply.github.com>", From dbdc4fc2a3c8e0d288ee5b16f21a457a5eaa54bd Mon Sep 17 00:00:00 2001 From: ryanshanz <92600751+ryanshanz@users.noreply.github.com> Date: Mon, 31 Mar 2025 00:33:39 -0400 Subject: [PATCH 52/57] fix: Copy and Import button layouts (#402) --- static/style.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/static/style.css b/static/style.css index 545567e..608ae17 100644 --- a/static/style.css +++ b/static/style.css @@ -561,6 +561,10 @@ aside { cursor: pointer; } +.copy, +.import { + margin: 5px; +} .subscribe, .filter, .copy, From 1fc36694932e90cef216700919c848fcea12792d Mon Sep 17 00:00:00 2001 From: Peter Lafreniere Date: Tue, 8 Apr 2025 21:17:26 -0400 Subject: [PATCH 53/57] fix: correct typo bockquote -> blockquote in template --- templates/comment.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/comment.html b/templates/comment.html index 36c9b60..3c008c7 100644 --- a/templates/comment.html +++ b/templates/comment.html @@ -41,7 +41,7 @@
{{ body|safe }}
{% endif %}
{% for c in replies -%}{{ c.render().unwrap()|safe }}{%- endfor %} - +
{% endif %} From e80ec5d16b856f19072e199beb7258414cce16a4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 9 Apr 2025 01:45:01 +0000 Subject: [PATCH 54/57] build(deps): bump tokio from 1.44.0 to 1.44.2 Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.44.0 to 1.44.2. - [Release notes](https://github.com/tokio-rs/tokio/releases) - [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.44.0...tokio-1.44.2) --- updated-dependencies: - dependency-name: tokio dependency-version: 1.44.2 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6722b4d..1d881f8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2058,9 +2058,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.44.0" +version = "1.44.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9975ea0f48b5aa3972bf2d888c238182458437cc2a19374b81b25cdf1023fb3a" +checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48" dependencies = [ "backtrace", "bytes", diff --git a/Cargo.toml b/Cargo.toml index bb76d0a..0596bc2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,7 +27,7 @@ hyper = { version = "0.14.31", features = ["full"] } percent-encoding = "2.3.1" route-recognizer = "0.3.1" serde_json = "1.0.133" -tokio = { version = "1.35.1", features = ["full"] } +tokio = { version = "1.44.2", features = ["full"] } time = { version = "0.3.31", features = ["local-offset"] } url = "2.5.0" rust-embed = { version = "8.1.0", features = ["include-exclude"] } From dcb507d56710a047c743fe79a5326e4f1ca930a6 Mon Sep 17 00:00:00 2001 From: Matthew Esposito Date: Mon, 21 Apr 2025 14:17:27 -0400 Subject: [PATCH 55/57] feat: Improve OAuth error handling with custom AuthError type and better timeout management --- src/oauth.rs | 70 +++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 53 insertions(+), 17 deletions(-) diff --git a/src/oauth.rs b/src/oauth.rs index 5627900..f5659da 100644 --- a/src/oauth.rs +++ b/src/oauth.rs @@ -15,6 +15,8 @@ const REDDIT_ANDROID_OAUTH_CLIENT_ID: &str = "ohXpoqrZYub1kg"; const AUTH_ENDPOINT: &str = "https://www.reddit.com"; +const OAUTH_TIMEOUT: Duration = Duration::from_secs(5); + // Spoofed client for Android devices #[derive(Debug, Clone, Default)] pub struct Oauth { @@ -32,24 +34,30 @@ impl Oauth { loop { let attempt = Self::new_with_timeout().await; match attempt { - Ok(Some(oauth)) => { + Ok(Ok(oauth)) => { info!("[✅] Successfully created OAuth client"); return oauth; } - Ok(None) => { - error!("Failed to create OAuth client. Retrying in 5 seconds..."); + Ok(Err(e)) => { + error!("Failed to create OAuth client: {}. Retrying in 5 seconds...", { + match e { + AuthError::Hyper(error) => error.to_string(), + AuthError::SerdeDeserialize(error) => error.to_string(), + AuthError::Field((value, error)) => format!("{error}\n{value}"), + } + }); } - Err(duration) => { - error!("Failed to create OAuth client in {duration:?}. Retrying in 5 seconds..."); + Err(_) => { + error!("Failed to create OAuth client before timeout. Retrying in 5 seconds..."); } } - tokio::time::sleep(Duration::from_secs(5)).await; + tokio::time::sleep(OAUTH_TIMEOUT).await; } } - async fn new_with_timeout() -> Result, Elapsed> { + async fn new_with_timeout() -> Result, Elapsed> { let mut oauth = Self::default(); - timeout(Duration::from_secs(5), oauth.login()).await.map(|result| result.map(|_| oauth)) + timeout(OAUTH_TIMEOUT, oauth.login()).await.map(|result: Result<(), AuthError>| result.map(|_| oauth)) } pub(crate) fn default() -> Self { @@ -66,7 +74,7 @@ impl Oauth { device, } } - async fn login(&mut self) -> Option<()> { + async fn login(&mut self) -> Result<(), AuthError> { // Construct URL for OAuth token let url = format!("{AUTH_ENDPOINT}/auth/v2/oauth/access-token/loid"); let mut builder = Request::builder().method(Method::POST).uri(&url); @@ -95,7 +103,7 @@ impl Oauth { // Send request let client: &once_cell::sync::Lazy> = &CLIENT; - let resp = client.request(request).await.ok()?; + let resp = client.request(request).await?; trace!("Received response with status {} and length {:?}", resp.status(), resp.headers().get("content-length")); trace!("OAuth headers: {:#?}", resp.headers()); @@ -106,30 +114,58 @@ impl Oauth { // Not worried about the privacy implications, since this is randomly changed // and really only as privacy-concerning as the OAuth token itself. if let Some(header) = resp.headers().get("x-reddit-loid") { - self.headers_map.insert("x-reddit-loid".to_owned(), header.to_str().ok()?.to_string()); + self.headers_map.insert("x-reddit-loid".to_owned(), header.to_str().unwrap().to_string()); } // Same with x-reddit-session if let Some(header) = resp.headers().get("x-reddit-session") { - self.headers_map.insert("x-reddit-session".to_owned(), header.to_str().ok()?.to_string()); + self.headers_map.insert("x-reddit-session".to_owned(), header.to_str().unwrap().to_string()); } trace!("Serializing response..."); // Serialize response - let body_bytes = hyper::body::to_bytes(resp.into_body()).await.ok()?; - let json: serde_json::Value = serde_json::from_slice(&body_bytes).ok()?; + let body_bytes = hyper::body::to_bytes(resp.into_body()).await?; + let json: serde_json::Value = serde_json::from_slice(&body_bytes)?; trace!("Accessing relevant fields..."); // Save token and expiry - self.token = json.get("access_token")?.as_str()?.to_string(); - self.expires_in = json.get("expires_in")?.as_u64()?; + self.token = json + .get("access_token") + .ok_or_else(|| AuthError::Field((json.clone(), "access_token")))? + .as_str() + .ok_or_else(|| AuthError::Field((json.clone(), "access_token: as_str")))? + .to_string(); + self.expires_in = json + .get("expires_in") + .ok_or_else(|| AuthError::Field((json.clone(), "expires_in")))? + .as_u64() + .ok_or_else(|| AuthError::Field((json.clone(), "expires_in: as_u64")))?; self.headers_map.insert("Authorization".to_owned(), format!("Bearer {}", self.token)); info!("[✅] Success - Retrieved token \"{}...\", expires in {}", &self.token[..32], self.expires_in); - Some(()) + Ok(()) + } +} + +#[derive(Debug)] +enum AuthError { + Hyper(hyper::Error), + SerdeDeserialize(serde_json::Error), + Field((serde_json::Value, &'static str)), +} + +impl From for AuthError { + fn from(err: hyper::Error) -> Self { + AuthError::Hyper(err) + } +} + +impl From for AuthError { + fn from(err: serde_json::Error) -> Self { + AuthError::SerdeDeserialize(err) } } From 6b0ee8eafbe57b785d70ee028c3f38c909209182 Mon Sep 17 00:00:00 2001 From: ayaka Date: Sat, 24 May 2025 01:02:46 +1200 Subject: [PATCH 56/57] quick bodge (fix later) --- Cargo.lock | 20 ++--- src/main.rs | 2 +- src/subreddit.rs | 186 +++++++++++++++++++++++++++++++++------- templates/settings.html | 16 ++-- 4 files changed, 176 insertions(+), 48 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1d881f8..9c5175e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1355,7 +1355,16 @@ dependencies = [ ] [[package]] -name = "redlib" +name = "redox_syscall" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redsunlib" version = "0.36.0" dependencies = [ "arc-swap", @@ -1402,15 +1411,6 @@ dependencies = [ "uuid", ] -[[package]] -name = "redox_syscall" -version = "0.5.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1" -dependencies = [ - "bitflags", -] - [[package]] name = "regex" version = "1.11.1" diff --git a/src/main.rs b/src/main.rs index ca3b550..2128ad7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -342,7 +342,7 @@ async fn main() { // Configure settings app.at("/settings").get(|r| settings::get(r).boxed()).post(|r| settings::set(r).boxed()); app.at("/settings/restore").get(|r| settings::restore(r).boxed()); - app.at("/settings/encoded-restore").post(|r| settings::encoded_restore(r).boxed()); + // app.at("/settings/encoded-restore").post(|r| settings::encoded_restore(r).boxed()); NOTE: TODO (Well, TOFIX) app.at("/settings/update").get(|r| settings::update(r).boxed()); // Mascots diff --git a/src/subreddit.rs b/src/subreddit.rs index 2eea14c..a9b5f21 100644 --- a/src/subreddit.rs +++ b/src/subreddit.rs @@ -3,13 +3,16 @@ use crate::{config, utils}; // CRATES use crate::utils::{ - catch_random, error, filter_posts, format_num, format_url, get_filters, nsfw_landing, param, redirect, rewrite_urls, setting, template, val, Post, Preferences, Subreddit, + catch_random, error, filter_posts, format_num, format_url, get_filters, info, nsfw_landing, param, redirect, rewrite_urls, setting, template, val, Post, Preferences, + Subreddit, }; use crate::{client::json, server::RequestExt, server::ResponseExt}; use cookie::Cookie; +use htmlescape::decode_html; use hyper::{Body, Request, Response}; use rinja::Template; +use chrono::DateTime; use once_cell::sync::Lazy; use regex::Regex; use time::{macros::format_description, Duration, OffsetDateTime}; @@ -66,6 +69,7 @@ pub async fn community(req: Request) -> Result, String> { let query = req.uri().query().unwrap_or_default().to_string(); let subscribed = setting(&req, "subscriptions"); let front_page = setting(&req, "front_page"); + let remove_default_feeds = setting(&req, "remove_default_feeds") == "on"; let post_sort = req.cookie("post_sort").map_or_else(|| "hot".to_string(), |c| c.value().to_string()); let sort = req.param("sort").unwrap_or_else(|| req.param("id").unwrap_or(post_sort)); @@ -78,6 +82,21 @@ pub async fn community(req: Request) -> Result, String> { } else { front_page.clone() }); + + if (sub_name == "popular" || sub_name == "all") && remove_default_feeds { + if subscribed.is_empty() { + return info(req, "Subscribe to some subreddits! (Default feeds disabled in settings)").await; + } else { + // If there are subscribed subs, but we get here, then the problem is that front_page pref is set to something besides default. + // Tell user to go to settings and change front page to default. + return info( + req, + "You have subscribed to some subreddits, but your front page is not set to default. Visit settings and change front page to default.", + ) + .await; + } + } + let quarantined = can_access_quarantine(&req, &sub_name) || root; // Handle random subreddits @@ -213,6 +232,41 @@ pub fn can_access_quarantine(req: &Request, sub: &str) -> bool { setting(req, &format!("allow_quaran_{}", sub.to_lowercase())).parse().unwrap_or_default() } +// Join items in chunks of 4000 bytes in length for cookies +pub fn join_until_size_limit(vec: &[T]) -> Vec { + let mut result = Vec::new(); + let mut list = String::new(); + let mut current_size = 0; + + for item in vec { + // Size in bytes + let item_size = item.to_string().len(); + // Use 4000 bytes to leave us some headroom because the name and options of the cookie count towards the 4096 byte cap + if current_size + item_size > 4000 { + // If last item add a seperator on the end of the list so it's interpreted properly in tanden with the next cookie + list.push('+'); + + // Push current list to result vector + result.push(list); + + // Reset the list variable so we can continue with only new items + list = String::new(); + } + // Add separator if not the first item + if !list.is_empty() { + list.push('+'); + } + // Add current item to list + list.push_str(&item.to_string()); + current_size = list.len() + item_size; + } + // Make sure to push whatever the remaining subreddits are there into the result vector + result.push(list); + + // Return resulting vector + result +} + // Sub, filter, unfilter, quicklist, unquicklist or unsub by setting subscription cookie using response "Set-Cookie" header pub async fn subscriptions_filters_quicklists(req: Request) -> Result, String> { let sub = req.param("sub").unwrap_or_default(); @@ -314,39 +368,112 @@ pub async fn subscriptions_filters_quicklists(req: Request) -> Result) -> Result, String> { .into_iter() .map(|post| Item { title: Some(post.title.to_string()), - link: Some(utils::get_post_url(&post)), + link: Some(format_url(&utils::get_post_url(&post))), author: Some(post.author.name), - content: Some(rewrite_urls(&post.body)), + content: Some(rewrite_urls(&decode_html(&post.body).unwrap())), + pub_date: Some(DateTime::from_timestamp(post.created_ts as i64, 0).unwrap_or_default().to_rfc2822()), description: Some(format!( "Comments", config::get_setting("REDLIB_FULL_URL").unwrap_or_default(), diff --git a/templates/settings.html b/templates/settings.html index 0aa9abb..2964d5f 100644 --- a/templates/settings.html +++ b/templates/settings.html @@ -37,12 +37,6 @@
Interface -
- - - -
+
+ + + +
@@ -219,7 +219,7 @@
{% endif %} -
+
{% endblock %} \ No newline at end of file From d8b547547b9cd182c555dccbad5d091aed648eed Mon Sep 17 00:00:00 2001 From: Ayaka Date: Sat, 24 May 2025 01:07:20 +1200 Subject: [PATCH 57/57] user flair with mixed RTL-LTR text, Fixes #23 --- static/style.css | 1 + 1 file changed, 1 insertion(+) diff --git a/static/style.css b/static/style.css index e08c101..1e723ed 100644 --- a/static/style.css +++ b/static/style.css @@ -1460,6 +1460,7 @@ a.search_subreddit:hover { border-radius: 5px; font-size: 12px; font-weight: bold; + unicode-bidi: isolate; } .comment_score {
-

Note: settings and subscriptions are saved in browser cookies. Clearing your cookies will reset them.


-

You can restore your current settings and subscriptions after clearing your cookies using this link.

+

Note: settings and subscriptions are saved in browser cookies. Clearing your cookies will reset them.

+
+ {% match prefs.to_urlencoded() %} + {% when Ok with (encoded_prefs) %} +

You can restore your current settings and subscriptions after clearing your cookies using this link.

+ {% when Err with (err) %} +

There was an error creating your restore link: {{ err }}

+

Please report this issue

+ {% endmatch %}