From 5d518cfc1890b64e8b76bfbae931c99a5def5fa5 Mon Sep 17 00:00:00 2001 From: Daniel Valentine Date: Sat, 3 Dec 2022 19:11:31 -0700 Subject: [PATCH 01/12] Cache result of `server::determine_compressor`. --- src/server.rs | 74 +++++++++++++++++++++++---------------------------- 1 file changed, 33 insertions(+), 41 deletions(-) diff --git a/src/server.rs b/src/server.rs index c277b6b..501b933 100644 --- a/src/server.rs +++ b/src/server.rs @@ -243,7 +243,7 @@ impl Server { match func.await { Ok(mut res) => { res.headers_mut().extend(def_headers); - let _ = compress_response(req_headers, &mut res).await; + let _ = compress_response(&req_headers, &mut res).await; Ok(res) } @@ -282,7 +282,7 @@ async fn new_boilerplate( ) -> Result, String> { match Response::builder().status(status).body(body) { Ok(mut res) => { - let _ = compress_response(req_headers, &mut res).await; + let _ = compress_response(&req_headers, &mut res).await; res.headers_mut().extend(default_headers.clone()); Ok(res) @@ -306,7 +306,8 @@ async fn new_boilerplate( /// Accept-Encoding: gzip, compress, br /// Accept-Encoding: br;q=1.0, gzip;q=0.8, *;q=0.1 /// ``` -fn determine_compressor(accept_encoding: &str) -> Option { +#[cached] +fn determine_compressor(accept_encoding: String) -> Option { if accept_encoding.is_empty() { return None; }; @@ -473,7 +474,7 @@ fn determine_compressor(accept_encoding: &str) -> Option { /// /// This function logs errors to stderr, but only in debug mode. No information /// is logged in release builds. -async fn compress_response(req_headers: HeaderMap, res: &mut Response) -> Result<(), String> { +async fn compress_response(req_headers: &HeaderMap, res: &mut Response) -> Result<(), String> { // Check if the data is eligible for compression. if let Some(hdr) = res.headers().get(header::CONTENT_TYPE) { match from_utf8(hdr.as_bytes()) { @@ -503,30 +504,22 @@ async fn compress_response(req_headers: HeaderMap, res: &mu return Ok(()); }; - // Quick and dirty closure for extracting a header from the request and - // returning it as a &str. - let get_req_header = |k: header::HeaderName| -> Option<&str> { - match req_headers.get(k) { - Some(hdr) => match from_utf8(hdr.as_bytes()) { - Ok(val) => Some(val), - - #[cfg(debug_assertions)] - Err(e) => { - dbg_msg!(e); - None - } - - #[cfg(not(debug_assertions))] - Err(_) => None, - }, - None => None, - } - }; - // Check to see which compressor is requested, and if we can use it. - let accept_encoding: &str = match get_req_header(header::ACCEPT_ENCODING) { - Some(val) => val, + let accept_encoding: String = match req_headers.get(header::ACCEPT_ENCODING) { None => return Ok(()), // Client requested no compression. + + Some(hdr) => match String::from_utf8(hdr.as_bytes().into()) { + Ok(val) => val, + + #[cfg(debug_assertions)] + Err(e) => { + dbg_msg!(e); + return Ok(()); + } + + #[cfg(not(debug_assertions))] + Err(_) => return Ok(()), + }, }; let compressor: CompressionType = match determine_compressor(accept_encoding) { @@ -636,18 +629,18 @@ mod tests { #[test] fn test_determine_compressor() { // Single compressor given. - assert_eq!(determine_compressor("unsupported"), None); - assert_eq!(determine_compressor("gzip"), Some(CompressionType::Gzip)); - assert_eq!(determine_compressor("*"), Some(DEFAULT_COMPRESSOR)); + assert_eq!(determine_compressor("unsupported".to_string()), None); + assert_eq!(determine_compressor("gzip".to_string()), Some(CompressionType::Gzip)); + assert_eq!(determine_compressor("*".to_string()), Some(DEFAULT_COMPRESSOR)); // Multiple compressors. - assert_eq!(determine_compressor("gzip, br"), Some(CompressionType::Brotli)); - assert_eq!(determine_compressor("gzip;q=0.8, br;q=0.3"), Some(CompressionType::Gzip)); - assert_eq!(determine_compressor("br, gzip"), Some(CompressionType::Brotli)); - assert_eq!(determine_compressor("br;q=0.3, gzip;q=0.4"), Some(CompressionType::Gzip)); + assert_eq!(determine_compressor("gzip, br".to_string()), Some(CompressionType::Brotli)); + assert_eq!(determine_compressor("gzip;q=0.8, br;q=0.3".to_string()), Some(CompressionType::Gzip)); + assert_eq!(determine_compressor("br, gzip".to_string()), Some(CompressionType::Brotli)); + assert_eq!(determine_compressor("br;q=0.3, gzip;q=0.4".to_string()), Some(CompressionType::Gzip)); // Invalid q-values. - assert_eq!(determine_compressor("gzip;q=NAN"), None); + assert_eq!(determine_compressor("gzip;q=NAN".to_string()), None); } #[test] @@ -672,9 +665,9 @@ mod tests { ] { // Determine what the expected encoding should be based on both the // specific encodings we accept. - let expected_encoding: CompressionType = match determine_compressor(accept_encoding) { + let expected_encoding: CompressionType = match determine_compressor(accept_encoding.to_string()) { Some(s) => s, - None => panic!("determine_compressor(accept_encoding) => None"), + None => panic!("determine_compressor(accept_encoding.to_string()) => None"), }; // Build headers with our Accept-Encoding. @@ -691,8 +684,8 @@ mod tests { .unwrap(); // Perform the compression. - if let Err(e) = block_on(compress_response(req_headers, &mut res)) { - panic!("compress_response(req_headers, &mut res) => Err(\"{}\")", e); + if let Err(e) = block_on(compress_response(&req_headers, &mut res)) { + panic!("compress_response(&req_headers, &mut res) => Err(\"{}\")", e); }; // If the content was compressed, we expect the Content-Encoding @@ -739,9 +732,8 @@ mod tests { }; let mut decompressed = Vec::::new(); - match io::copy(&mut decoder, &mut decompressed) { - Ok(_) => {} - Err(e) => panic!("{}", e), + if let Err(e) = io::copy(&mut decoder, &mut decompressed) { + panic!("{}", e); }; assert!(decompressed.eq(&expected_lorem_ipsum)); From 37d1939dc02ecc641636a9ca36e24f15163fd620 Mon Sep 17 00:00:00 2001 From: Daniel Valentine Date: Tue, 13 Dec 2022 21:15:28 -0700 Subject: [PATCH 02/12] Fix #658. Dimensions for embedded video in post are explicitly set only when defined by Reddit. c/o: NKIPSC <15067635+NKIPSC@users.noreply.github.com> --- Cargo.lock | 2 +- Cargo.toml | 2 +- templates/utils.html | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 87c1b9a..2865f62 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -664,7 +664,7 @@ dependencies = [ [[package]] name = "libreddit" -version = "0.25.0" +version = "0.25.1" dependencies = [ "askama", "async-recursion", diff --git a/Cargo.toml b/Cargo.toml index 5136235..157ed31 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ name = "libreddit" description = " Alternative private front-end to Reddit" license = "AGPL-3.0" repository = "https://github.com/spikecodes/libreddit" -version = "0.25.0" +version = "0.25.1" authors = ["spikecodes <19519553+spikecodes@users.noreply.github.com>"] edition = "2021" diff --git a/templates/utils.html b/templates/utils.html index 87d47a3..639b0d8 100644 --- a/templates/utils.html +++ b/templates/utils.html @@ -115,7 +115,7 @@ {% if prefs.use_hls == "on" && !post.media.alt_url.is_empty() %}
-
{% else if (prefs.layout.is_empty() || prefs.layout == "card") && post.post_type == "gif" %}
- +
{% else if (prefs.layout.is_empty() || prefs.layout == "card") && post.post_type == "video" %} {% if prefs.use_hls == "on" && !post.media.alt_url.is_empty() %}
-
{% else %}
- +
{% call render_hls_notification(format!("{}%23{}", &self.url[1..].replace("&", "%26").replace("+", "%2B"), post.id)) %} {% endif %} From ab30b8bbecc187284afd05a36b3fa39e37936795 Mon Sep 17 00:00:00 2001 From: gmnsii <95436780+gmnsii@users.noreply.github.com> Date: Sat, 31 Dec 2022 18:11:59 -0800 Subject: [PATCH 03/12] Bugfix: 'all posts are hidden because NSFW' when no posts where found (#666) * Fix 'all_posts_hidden_nsfw' when there are no posts. If a search query yielded no results and the user set nsfw posts to be hidden, libreddit would show 'All posts are hidden because they are NSFW. Enable "Show NSFW posts" in settings to view'. This is fixed by verifying tnat posts.len > 0 before setting 'all_posts_hidden_nsfw' to true. * Add a message when no posts were found. * Delete 2 --- src/search.rs | 6 +++++- src/subreddit.rs | 6 +++++- src/user.rs | 6 +++++- templates/search.html | 4 ++++ templates/subreddit.html | 4 ++++ templates/user.html | 4 ++++ 6 files changed, 27 insertions(+), 3 deletions(-) diff --git a/src/search.rs b/src/search.rs index 0a62b06..9fbe77a 100644 --- a/src/search.rs +++ b/src/search.rs @@ -44,6 +44,7 @@ struct SearchTemplate { all_posts_filtered: bool, /// Whether all posts were hidden because they are NSFW (and user has disabled show NSFW) all_posts_hidden_nsfw: bool, + no_posts: bool, } // SERVICES @@ -103,12 +104,14 @@ pub async fn find(req: Request) -> Result, String> { is_filtered: true, all_posts_filtered: false, all_posts_hidden_nsfw: false, + no_posts: false, }) } else { match Post::fetch(&path, quarantined).await { Ok((mut posts, after)) => { let (_, all_posts_filtered) = filter_posts(&mut posts, &filters); - let all_posts_hidden_nsfw = posts.iter().all(|p| p.flags.nsfw) && setting(&req, "show_nsfw") != "on"; + let no_posts = posts.is_empty(); + let all_posts_hidden_nsfw = !no_posts && (posts.iter().all(|p| p.flags.nsfw) && setting(&req, "show_nsfw") != "on"); template(SearchTemplate { posts, subreddits, @@ -127,6 +130,7 @@ pub async fn find(req: Request) -> Result, String> { is_filtered: false, all_posts_filtered, all_posts_hidden_nsfw, + no_posts, }) } Err(msg) => { diff --git a/src/subreddit.rs b/src/subreddit.rs index 4aff027..ef511c2 100644 --- a/src/subreddit.rs +++ b/src/subreddit.rs @@ -26,6 +26,7 @@ struct SubredditTemplate { all_posts_filtered: bool, /// Whether all posts were hidden because they are NSFW (and user has disabled show NSFW) all_posts_hidden_nsfw: bool, + no_posts: bool, } #[derive(Template)] @@ -114,12 +115,14 @@ pub async fn community(req: Request) -> Result, String> { is_filtered: true, all_posts_filtered: false, all_posts_hidden_nsfw: false, + no_posts: false, }) } else { match Post::fetch(&path, quarantined).await { Ok((mut posts, after)) => { let (_, all_posts_filtered) = filter_posts(&mut posts, &filters); - let all_posts_hidden_nsfw = posts.iter().all(|p| p.flags.nsfw) && setting(&req, "show_nsfw") != "on"; + let no_posts = posts.is_empty(); + let all_posts_hidden_nsfw = !no_posts && (posts.iter().all(|p| p.flags.nsfw) && setting(&req, "show_nsfw") != "on"); template(SubredditTemplate { sub, posts, @@ -131,6 +134,7 @@ pub async fn community(req: Request) -> Result, String> { is_filtered: false, all_posts_filtered, all_posts_hidden_nsfw, + no_posts, }) } Err(msg) => match msg.as_str() { diff --git a/src/user.rs b/src/user.rs index 8c0540c..6c991ef 100644 --- a/src/user.rs +++ b/src/user.rs @@ -26,6 +26,7 @@ struct UserTemplate { all_posts_filtered: bool, /// Whether all posts were hidden because they are NSFW (and user has disabled show NSFW) all_posts_hidden_nsfw: bool, + no_posts: bool, } // FUNCTIONS @@ -61,13 +62,15 @@ pub async fn profile(req: Request) -> Result, String> { is_filtered: true, all_posts_filtered: false, all_posts_hidden_nsfw: false, + no_posts: false, }) } else { // Request user posts/comments from Reddit match Post::fetch(&path, false).await { Ok((mut posts, after)) => { let (_, all_posts_filtered) = filter_posts(&mut posts, &filters); - let all_posts_hidden_nsfw = posts.iter().all(|p| p.flags.nsfw) && setting(&req, "show_nsfw") != "on"; + let no_posts = posts.is_empty(); + let all_posts_hidden_nsfw = !no_posts && (posts.iter().all(|p| p.flags.nsfw) && setting(&req, "show_nsfw") != "on"); template(UserTemplate { user, posts, @@ -80,6 +83,7 @@ pub async fn profile(req: Request) -> Result, String> { is_filtered: false, all_posts_filtered, all_posts_hidden_nsfw, + no_posts, }) } // If there is an error show error page diff --git a/templates/search.html b/templates/search.html index 43fadb4..b9742f6 100644 --- a/templates/search.html +++ b/templates/search.html @@ -61,6 +61,10 @@ All posts are hidden because they are NSFW. Enable "Show NSFW posts" in settings to view. {% endif %} + {% if no_posts %} +
No posts were found.
+ {% endif %} + {% if all_posts_filtered %} (All content on this page has been filtered) {% else if is_filtered %} diff --git a/templates/subreddit.html b/templates/subreddit.html index 4fdad65..9ad1932 100644 --- a/templates/subreddit.html +++ b/templates/subreddit.html @@ -50,6 +50,10 @@
All posts are hidden because they are NSFW. Enable "Show NSFW posts" in settings to view.
{% endif %} + {% if no_posts %} +
No posts were found.
+ {% endif %} + {% if all_posts_filtered %}
(All content on this page has been filtered)
{% else %} diff --git a/templates/user.html b/templates/user.html index 04dc4e6..a72cce0 100644 --- a/templates/user.html +++ b/templates/user.html @@ -36,6 +36,10 @@
All posts are hidden because they are NSFW. Enable "Show NSFW posts" in settings to view.
{% endif %} + {% if no_posts %} +
No posts were found.
+ {% endif %} + {% if all_posts_filtered %}
(All content on this page has been filtered)
{% else %} From 9e434e7db6deccd2c6db7bb63a906453e9f963a6 Mon Sep 17 00:00:00 2001 From: gmnsii <95436780+gmnsii@users.noreply.github.com> Date: Sat, 31 Dec 2022 19:57:42 -0800 Subject: [PATCH 04/12] Search - add support for raw reddit links (#663) * Search - add support for raw reddit links If a search query starts with 'https://www.reddit.com/' or 'https://old.reddit.com/', this prefix will be truncated and the query will be processed normally. For example, a search query 'https://www.reddit.com/r/rust' will redirect to r/rust. * Search - support a wider variety of reddit links. Add once cell dependency for static regex support (avoid compiling the same regex multiple times). All search queries are now matched against a regex (provided by @Daniel-Valentine) that determines if it is a reddit link. If it is, the prefix specifying the reddit instance will be truncated from the query that will then be processed normally. For example, the query 'https://www.reddit.com/r/rust' will be treated the same way as the query 'r/rust'. --- Cargo.lock | 1 + Cargo.toml | 1 + src/search.rs | 8 +++++++- 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 2865f62..6945ebe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -677,6 +677,7 @@ dependencies = [ "hyper-rustls", "libflate", "lipsum", + "once_cell", "percent-encoding", "regex", "route-recognizer", diff --git a/Cargo.toml b/Cargo.toml index 157ed31..c1b2548 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,7 @@ url = "2.3.1" rust-embed = { version = "6.4.2", features = ["include-exclude"] } libflate = "1.2.0" brotli = { version = "3.3.4", features = ["std"] } +once_cell = "1.16.0" [dev-dependencies] lipsum = "0.8.2" diff --git a/src/search.rs b/src/search.rs index 9fbe77a..87965c3 100644 --- a/src/search.rs +++ b/src/search.rs @@ -7,6 +7,8 @@ use crate::{ }; use askama::Template; use hyper::{Body, Request, Response}; +use once_cell::sync::Lazy; +use regex::Regex; // STRUCTS struct SearchParams { @@ -47,11 +49,15 @@ struct SearchTemplate { no_posts: bool, } +// Regex matched against search queries to determine if they are reddit urls. +static REDDIT_URL_MATCH: Lazy = Lazy::new(|| Regex::new(r"^https?://([^\./]+\.)*reddit.com/").unwrap()); + // SERVICES pub async fn find(req: Request) -> Result, String> { let nsfw_results = if setting(&req, "show_nsfw") == "on" { "&include_over_18=on" } else { "" }; let path = format!("{}.json?{}{}&raw_json=1", req.uri().path(), req.uri().query().unwrap_or_default(), nsfw_results); - let query = param(&path, "q").unwrap_or_default(); + let mut query = param(&path, "q").unwrap_or_default(); + query = REDDIT_URL_MATCH.replace(&query, "").to_string(); if query.is_empty() { return Ok(redirect("/".to_string())); From b5d04f1a50681cc78fc387d04e2f4c59f363d7a0 Mon Sep 17 00:00:00 2001 From: Daniel Valentine Date: Sat, 31 Dec 2022 21:34:15 -0700 Subject: [PATCH 05/12] v0.25.2 --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6945ebe..f52164a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -664,7 +664,7 @@ dependencies = [ [[package]] name = "libreddit" -version = "0.25.1" +version = "0.25.2" dependencies = [ "askama", "async-recursion", diff --git a/Cargo.toml b/Cargo.toml index c1b2548..e67f5e1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ name = "libreddit" description = " Alternative private front-end to Reddit" license = "AGPL-3.0" repository = "https://github.com/spikecodes/libreddit" -version = "0.25.1" +version = "0.25.2" authors = ["spikecodes <19519553+spikecodes@users.noreply.github.com>"] edition = "2021" From 9178b50b73c91289bf296005c429a0597baeb7c6 Mon Sep 17 00:00:00 2001 From: Rupert Angermeier Date: Sun, 1 Jan 2023 09:56:09 +0100 Subject: [PATCH 06/12] fix a11y and HTML issues on settings page (#662) - connect labels with corresponding form controls - use fieldsets to group form sections - don't nest details/summary element into label --- static/style.css | 39 +++++++----- templates/settings.html | 133 +++++++++++++++++++++------------------- 2 files changed, 93 insertions(+), 79 deletions(-) diff --git a/static/style.css b/static/style.css index 500646d..a0d4b69 100644 --- a/static/style.css +++ b/static/style.css @@ -1118,22 +1118,16 @@ summary.comment_data { } .prefs { - display: flex; - flex-direction: column; - justify-content: space-between; - padding: 20px; + padding: 10px 20px 20px; background: var(--post); border-radius: 5px; margin-bottom: 20px; } -.prefs > div { - display: flex; - justify-content: space-between; - width: 100%; - height: 35px; - align-items: center; - margin-top: 7px; +.prefs fieldset { + border: 0; + padding: 10px 0; + margin: 0 0 5px; } .prefs legend { @@ -1141,11 +1135,25 @@ summary.comment_data { border-bottom: 1px solid var(--highlighted); font-size: 18px; padding-bottom: 10px; + margin-bottom: 7px; + width: 100%; + float: left; /* places the legend inside the (invisible) border, instead of vertically centered on top border*/ } -.prefs legend:not(:first-child) { - padding-top: 10px; - margin-top: 15px; +.prefs-group { + display: flex; + width: 100%; + height: 35px; + align-items: center; + margin-top: 7px; +} + +.prefs-group > *:not(:last-child) { + margin-right: 1ch; +} + +.prefs-group > *:last-child { + margin-left: auto; } .prefs select { @@ -1163,7 +1171,8 @@ aside.prefs { background: var(--highlighted); padding: 10px 15px; border-radius: 5px; - margin-top: 20px; + margin-top: 5px; + width: 100% } input[type="submit"] { diff --git a/templates/settings.html b/templates/settings.html index ed5809d..b4bab8c 100644 --- a/templates/settings.html +++ b/templates/settings.html @@ -11,74 +11,79 @@
- Appearance -
- - -
- Interface -
- - -
-
- - -
-
- - - -
- Content -
- - -
-
- - -
-
- - - -
-
- - - -
-
- - - -
-
-
+
+ + + +
+
From a49d399f72d334adb5307eda2d23fbfa14d258be Mon Sep 17 00:00:00 2001 From: Spenser Black Date: Sun, 1 Jan 2023 13:38:52 -0500 Subject: [PATCH 07/12] Link to `libreddit/libreddit` and open in new tab This sets the target of the "code" link to `_blank`, so that it will open in a new tab in browsers. Because the GitHub page is a different context from libreddit, and accessing the repository doesn't imply that the user is finished browsing libreddit, this seemed reasonable. This also changes the link from `spikecodes/libreddit` to `libreddit/libreddit`. --- templates/base.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/base.html b/templates/base.html index e9b51ec..bbea3bf 100644 --- a/templates/base.html +++ b/templates/base.html @@ -48,7 +48,7 @@ - + code code From 9c938c62103d4fd6b7289b97a598f59a24dc32a4 Mon Sep 17 00:00:00 2001 From: tirz <36501933+tirz@users.noreply.github.com> Date: Sun, 1 Jan 2023 22:33:31 +0100 Subject: [PATCH 08/12] build: enable LTO, set codegen-unit to 1 and strip the binary (#467) Co-authored-by: Spike <19519553+spikecodes@users.noreply.github.com> --- Cargo.toml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index e67f5e1..cc7f517 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,3 +31,8 @@ once_cell = "1.16.0" [dev-dependencies] lipsum = "0.8.2" + +[profile.release] +codegen-units = 1 +lto = true +strip = true \ No newline at end of file From 6cf374864263132912ea8fcf7a864619d1382cd2 Mon Sep 17 00:00:00 2001 From: elliot <75391956+ellieeet123@users.noreply.github.com> Date: Sun, 1 Jan 2023 17:06:58 -0600 Subject: [PATCH 09/12] Fix for #675 /:id route now accepts 7 character post IDs. --- src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index 3b45bd2..25c2aea 100644 --- a/src/main.rs +++ b/src/main.rs @@ -289,7 +289,7 @@ async fn main() { Some("best" | "hot" | "new" | "top" | "rising" | "controversial") => subreddit::community(req).await, // Short link for post - Some(id) if (5..7).contains(&id.len()) => match canonical_path(format!("/{}", id)).await { + Some(id) if (5..8).contains(&id.len()) => match canonical_path(format!("/{}", id)).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, From c15f305be0c97730f91694146c493fdccdc3371e Mon Sep 17 00:00:00 2001 From: Daniel Valentine Date: Sun, 1 Jan 2023 23:54:35 -0700 Subject: [PATCH 10/12] v0.25.3 --- Cargo.lock | 2 +- Cargo.toml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f52164a..e947b07 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -664,7 +664,7 @@ dependencies = [ [[package]] name = "libreddit" -version = "0.25.2" +version = "0.25.3" dependencies = [ "askama", "async-recursion", diff --git a/Cargo.toml b/Cargo.toml index cc7f517..1375851 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ name = "libreddit" description = " Alternative private front-end to Reddit" license = "AGPL-3.0" repository = "https://github.com/spikecodes/libreddit" -version = "0.25.2" +version = "0.25.3" authors = ["spikecodes <19519553+spikecodes@users.noreply.github.com>"] edition = "2021" @@ -35,4 +35,4 @@ lipsum = "0.8.2" [profile.release] codegen-units = 1 lto = true -strip = true \ No newline at end of file +strip = true From c83a4e0cc87ef82e2fc4ea9031393533744a3d54 Mon Sep 17 00:00:00 2001 From: Daniel Valentine Date: Tue, 3 Jan 2023 02:39:45 -0700 Subject: [PATCH 11/12] Landing page for NSFW content, SFW-only mode (#656) Co-authored-by: Matt Co-authored-by: Spike <19519553+spikecodes@users.noreply.github.com> --- README.md | 12 +++++- app.json | 3 ++ src/duplicates.rs | 12 +++++- src/post.rs | 10 ++++- src/search.rs | 9 ++++- src/subreddit.rs | 9 ++++- src/user.rs | 12 +++++- src/utils.rs | 82 ++++++++++++++++++++++++++++++++++++++ static/style.css | 52 ++++++++++++++++++++++-- templates/base.html | 5 +++ templates/nsfwlanding.html | 28 +++++++++++++ templates/settings.html | 6 +++ 12 files changed, 227 insertions(+), 13 deletions(-) create mode 100644 templates/nsfwlanding.html diff --git a/README.md b/README.md index 891e6c9..5f3c647 100644 --- a/README.md +++ b/README.md @@ -182,9 +182,17 @@ Once installed, deploy Libreddit to `0.0.0.0:8080` by running: libreddit ``` -## Change Default Settings +## Instance settings -Assign a default value for each setting by passing environment variables to Libreddit in the format `LIBREDDIT_DEFAULT_{X}`. Replace `{X}` with the setting name (see list below) in capital letters. +Assign a default value for each instance-specific setting by passing environment variables to Libreddit in the format `LIBREDDIT_{X}`. Replace `{X}` with the setting name (see list below) in capital letters. + +|Name|Possible values|Default value|Description| +|-|-|-|-| +| `SFW_ONLY` | `["on", "off"]` | `off` | Enables SFW-only mode for the instance, i.e. all NSFW content is filtered. | + +## Default User Settings + +Assign a default value for each user-modifiable setting by passing environment variables to Libreddit in the format `LIBREDDIT_DEFAULT_{Y}`. Replace `{Y}` with the setting name (see list below) in capital letters. | Name | Possible values | Default value | |-------------------------|-----------------------------------------------------------------------------------------------------|---------------| diff --git a/app.json b/app.json index fd41fc8..48b6f1d 100644 --- a/app.json +++ b/app.json @@ -40,6 +40,9 @@ }, "LIBREDDIT_HIDE_HLS_NOTIFICATION": { "required": false + }, + "LIBREDDIT_SFW_ONLY": { + "required": false } } } diff --git a/src/duplicates.rs b/src/duplicates.rs index 6a64fc8..d68d1b3 100644 --- a/src/duplicates.rs +++ b/src/duplicates.rs @@ -3,7 +3,7 @@ use crate::client::json; use crate::server::RequestExt; use crate::subreddit::{can_access_quarantine, quarantine}; -use crate::utils::{error, filter_posts, get_filters, parse_post, template, Post, Preferences}; +use crate::utils::{error, filter_posts, get_filters, nsfw_landing, parse_post, setting, template, Post, Preferences}; use askama::Template; use hyper::{Body, Request, Response}; @@ -65,8 +65,16 @@ pub async fn item(req: Request) -> Result, String> { match json(path, quarantined).await { // Process response JSON. Ok(response) => { - let filters = get_filters(&req); let post = parse_post(&response[0]["data"]["children"][0]).await; + + // Return landing page if this post if this Reddit deems this post + // NSFW, but we have also disabled the display of NSFW content + // or if the instance is SFW-only. + if post.nsfw && (setting(&req, "show_nsfw") != "on" || crate::utils::sfw_only()) { + return Ok(nsfw_landing(req).await.unwrap_or_default()); + } + + let filters = get_filters(&req); let (duplicates, num_posts_filtered, all_posts_filtered) = parse_duplicates(&response[1], &filters).await; // These are the values for the "before=", "after=", and "sort=" diff --git a/src/post.rs b/src/post.rs index e467fe7..1423e60 100644 --- a/src/post.rs +++ b/src/post.rs @@ -3,7 +3,7 @@ use crate::client::json; use crate::server::RequestExt; use crate::subreddit::{can_access_quarantine, quarantine}; use crate::utils::{ - error, format_num, get_filters, param, parse_post, rewrite_urls, setting, template, time, val, Author, Awards, Comment, Flair, FlairPart, Post, Preferences, + error, format_num, get_filters, nsfw_landing, param, parse_post, rewrite_urls, setting, template, time, val, Author, Awards, Comment, Flair, FlairPart, Post, Preferences, }; use hyper::{Body, Request, Response}; @@ -55,6 +55,14 @@ pub async fn item(req: Request) -> Result, String> { Ok(response) => { // Parse the JSON into Post and Comment structs let post = parse_post(&response[0]["data"]["children"][0]).await; + + // Return landing page if this post if this Reddit deems this post + // NSFW, but we have also disabled the display of NSFW content + // or if the instance is SFW-only. + if post.nsfw && (setting(&req, "show_nsfw") != "on" || crate::utils::sfw_only()) { + return Ok(nsfw_landing(req).await.unwrap_or_default()); + } + let comments = parse_comments(&response[1], &post.permalink, &post.author.name, highlighted_comment, &get_filters(&req)); let url = req.uri().to_string(); diff --git a/src/search.rs b/src/search.rs index 87965c3..7158fdc 100644 --- a/src/search.rs +++ b/src/search.rs @@ -1,5 +1,5 @@ // CRATES -use crate::utils::{catch_random, error, filter_posts, format_num, format_url, get_filters, param, redirect, setting, template, val, Post, Preferences}; +use crate::utils::{self, catch_random, error, filter_posts, format_num, format_url, get_filters, param, redirect, setting, template, val, Post, Preferences}; use crate::{ client::json, subreddit::{can_access_quarantine, quarantine}, @@ -54,7 +54,12 @@ static REDDIT_URL_MATCH: Lazy = Lazy::new(|| Regex::new(r"^https?://([^\. // SERVICES pub async fn find(req: Request) -> Result, String> { - let nsfw_results = if setting(&req, "show_nsfw") == "on" { "&include_over_18=on" } else { "" }; + // This ensures that during a search, no NSFW posts are fetched at all + let nsfw_results = if setting(&req, "show_nsfw") == "on" && !utils::sfw_only() { + "&include_over_18=on" + } else { + "" + }; let path = format!("{}.json?{}{}&raw_json=1", req.uri().path(), req.uri().query().unwrap_or_default(), nsfw_results); let mut query = param(&path, "q").unwrap_or_default(); query = REDDIT_URL_MATCH.replace(&query, "").to_string(); diff --git a/src/subreddit.rs b/src/subreddit.rs index ef511c2..af87d93 100644 --- a/src/subreddit.rs +++ b/src/subreddit.rs @@ -1,6 +1,6 @@ // CRATES use crate::utils::{ - catch_random, error, filter_posts, format_num, format_url, get_filters, param, redirect, rewrite_urls, setting, template, val, Post, Preferences, Subreddit, + catch_random, error, filter_posts, format_num, format_url, get_filters, nsfw_landing, param, redirect, rewrite_urls, setting, template, val, Post, Preferences, Subreddit, }; use crate::{client::json, server::ResponseExt, RequestExt}; use askama::Template; @@ -97,6 +97,12 @@ pub async fn community(req: Request) -> Result, String> { } }; + // Return landing page if this post if this is NSFW community but the user + // has disabled the display of NSFW content or if the instance is SFW-only. + if sub.nsfw && (setting(&req, "show_nsfw") != "on" || crate::utils::sfw_only()) { + return Ok(nsfw_landing(req).await.unwrap_or_default()); + } + let path = format!("/r/{}/{}.json?{}&raw_json=1", sub_name.clone(), sort, req.uri().query().unwrap_or_default()); 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"); @@ -424,5 +430,6 @@ async fn subreddit(sub: &str, quarantined: bool) -> Result { members: format_num(members), active: format_num(active), wiki: res["data"]["wiki_enabled"].as_bool().unwrap_or_default(), + nsfw: res["data"]["over18"].as_bool().unwrap_or_default(), }) } diff --git a/src/user.rs b/src/user.rs index 6c991ef..3620fce 100644 --- a/src/user.rs +++ b/src/user.rs @@ -1,7 +1,7 @@ // CRATES use crate::client::json; use crate::server::RequestExt; -use crate::utils::{error, filter_posts, format_url, get_filters, param, setting, template, Post, Preferences, User}; +use crate::utils::{error, filter_posts, format_url, get_filters, nsfw_landing, param, setting, template, Post, Preferences, User}; use askama::Template; use hyper::{Body, Request, Response}; use time::{macros::format_description, OffsetDateTime}; @@ -46,8 +46,17 @@ pub async fn profile(req: Request) -> Result, String> { // Retrieve other variables from Libreddit request let sort = param(&path, "sort").unwrap_or_default(); let username = req.param("name").unwrap_or_default(); + + // Retrieve info from user about page. let user = user(&username).await.unwrap_or_default(); + // Return landing page if this post if this Reddit deems this user NSFW, + // but we have also disabled the display of NSFW content or if the instance + // is SFW-only. + if user.nsfw && (setting(&req, "show_nsfw") != "on" || crate::utils::sfw_only()) { + return Ok(nsfw_landing(req).await.unwrap_or_default()); + } + let filters = get_filters(&req); if filters.contains(&["u_", &username].concat()) { template(UserTemplate { @@ -115,6 +124,7 @@ async fn user(name: &str) -> Result { created: created.format(format_description!("[month repr:short] [day] '[year repr:last_two]")).unwrap_or_default(), banner: about("banner_img"), description: about("public_description"), + nsfw: res["data"]["subreddit"]["over_18"].as_bool().unwrap_or_default(), } }) } diff --git a/src/utils.rs b/src/utils.rs index 06237e9..fee97e9 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -9,6 +9,7 @@ use regex::Regex; use rust_embed::RustEmbed; use serde_json::Value; use std::collections::{HashMap, HashSet}; +use std::env; use std::str::FromStr; use time::{macros::format_description, Duration, OffsetDateTime}; use url::Url; @@ -28,6 +29,16 @@ macro_rules! dbg_msg { }; } +/// Identifies whether or not the page is a subreddit, a user page, or a post. +/// This is used by the NSFW landing template to determine the mesage to convey +/// to the user. +#[derive(PartialEq, Eq)] +pub enum ResourceType { + Subreddit, + User, + Post, +} + // Post flair with content, background color and foreground color pub struct Flair { pub flair_parts: Vec, @@ -229,6 +240,7 @@ pub struct Post { pub comments: (String, String), pub gallery: Vec, pub awards: Awards, + pub nsfw: bool, } impl Post { @@ -329,6 +341,7 @@ impl Post { comments: format_num(data["num_comments"].as_i64().unwrap_or_default()), gallery, awards, + nsfw: post["data"]["over_18"].as_bool().unwrap_or_default(), }); } @@ -420,6 +433,27 @@ pub struct ErrorTemplate { 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. +#[derive(Template)] +#[template(path = "nsfwlanding.html")] +pub struct NSFWLandingTemplate { + /// Identifier for the resource. This is either a subreddit name or a + /// username. (In the case of the latter, set is_user to true.) + pub res: String, + + /// Identifies whether or not the resource is a subreddit, a user page, + /// or a post. + pub res_type: ResourceType, + + /// User preferences. + pub prefs: Preferences, + + /// Request URL. + pub url: String, +} + #[derive(Default)] // User struct containing metadata about user pub struct User { @@ -430,6 +464,7 @@ pub struct User { pub created: String, pub banner: String, pub description: String, + pub nsfw: bool, } #[derive(Default)] @@ -444,6 +479,7 @@ pub struct Subreddit { pub members: (String, String), pub active: (String, String), pub wiki: bool, + pub nsfw: bool, } // Parser for query params, used in sorting (eg. /r/rust/?sort=hot) @@ -617,6 +653,7 @@ pub async fn parse_post(post: &serde_json::Value) -> Post { comments: format_num(post["data"]["num_comments"].as_i64().unwrap_or_default()), gallery, awards, + nsfw: post["data"]["over_18"].as_bool().unwrap_or_default(), } } @@ -829,6 +866,51 @@ pub async fn error(req: Request, msg: impl ToString) -> Result bool { + match env::var("LIBREDDIT_SFW_ONLY") { + Ok(val) => val == "on", + Err(_) => false, + } +} + +/// Renders the landing page for NSFW content when the user has not enabled +/// "show NSFW posts" in settings. +pub async fn nsfw_landing(req: Request) -> Result, String> { + let res_type: ResourceType; + let url = req.uri().to_string(); + + // Determine from the request URL if the resource is a subreddit, a user + // page, or a post. + let res: String = if !req.param("name").unwrap_or_default().is_empty() { + res_type = ResourceType::User; + req.param("name").unwrap_or_default() + } else if !req.param("id").unwrap_or_default().is_empty() { + res_type = ResourceType::Post; + req.param("id").unwrap_or_default() + } else { + res_type = ResourceType::Subreddit; + req.param("sub").unwrap_or_default() + }; + + let body = NSFWLandingTemplate { + res, + res_type, + prefs: Preferences::new(req), + url, + } + .render() + .unwrap_or_default(); + + Ok(Response::builder().status(403).header("content-type", "text/html").body(body.into()).unwrap_or_default()) +} + #[cfg(test)] mod tests { use super::{format_num, format_url, rewrite_urls}; diff --git a/static/style.css b/static/style.css index a0d4b69..05c493a 100644 --- a/static/style.css +++ b/static/style.css @@ -160,16 +160,35 @@ main { overflow: inherit; } -footer { +/* Body footer. */ +body > footer { + display: flex; + justify-content: center; + margin: 20px; +} + +body > footer > div#sfw-only { + color: var(--green); + border: 1px solid var(--green); + padding: 5px; + box-sizing: border-box; + border-radius: 5px; +} +/* / Body footer. */ + +/* Footer in content block. */ +main > * > footer { display: flex; justify-content: center; margin-top: 20px; } -footer > a { +main > * > footer > a { margin-right: 5px; } +/* / Footer in content block. */ + button { background: none; border: none; @@ -485,7 +504,7 @@ button.submit:hover > svg { stroke: var(--accent); } overflow-x: auto; } -#sort_options, #listing_options, footer > a { +#sort_options, #listing_options, main > * > footer > a { border-radius: 5px; align-items: center; box-shadow: var(--shadow); @@ -494,7 +513,7 @@ button.submit:hover > svg { stroke: var(--accent); } overflow: hidden; } -#sort_options > a, #listing_options > a, footer > a { +#sort_options > a, #listing_options > a, main > * > footer > a { color: var(--text); padding: 10px 20px; text-align: center; @@ -1315,6 +1334,31 @@ td, th { color: var(--accent); } +/* NSFW Landing Page */ + +#nsfw_landing { + display: inline-block; + text-align: center; + width: 100%; +} + +#nsfw_landing h1 { + display: inline-block; + margin-bottom: 20px; + text-align: center; + width: 100%; +} + +#nsfw_landing p { + display: inline-block; + text-align: center; + width: 100%; +} + +#nsfw_landing a { + color: var(--accent); +} + /* Mobile */ @media screen and (max-width: 800px) { diff --git a/templates/base.html b/templates/base.html index bbea3bf..dd882d8 100644 --- a/templates/base.html +++ b/templates/base.html @@ -65,5 +65,10 @@ {% endblock %} {% endblock %} + {% block footer %} + {% if crate::utils::sfw_only() %} +
This instance of Libreddit is SFW-only.
+ {% endif %} + {% endblock %} diff --git a/templates/nsfwlanding.html b/templates/nsfwlanding.html new file mode 100644 index 0000000..f6287a3 --- /dev/null +++ b/templates/nsfwlanding.html @@ -0,0 +1,28 @@ +{% extends "base.html" %} +{% block title %}NSFW content gated{% endblock %} +{% block sortstyle %}{% endblock %} +{% block content %} +
+{% endblock %} +{% block footer %} +{% endblock %} diff --git a/templates/settings.html b/templates/settings.html index b4bab8c..530176e 100644 --- a/templates/settings.html +++ b/templates/settings.html @@ -54,6 +54,7 @@ {% call utils::options(prefs.comment_sort, ["confidence", "top", "new", "controversial", "old"], "confidence") %}
+ {% if !crate::utils::sfw_only() %}
@@ -64,6 +65,7 @@
+ {% endif %}
@@ -121,6 +123,10 @@

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.

+
+ {% if crate::utils::sfw_only() %} +

This instance is SFW-only. It will block all NSFW content.

+ {% endif %}
From 4817f51bc0d0b499ec1123b4d930bfb588e8334a Mon Sep 17 00:00:00 2001 From: Daniel Valentine Date: Tue, 3 Jan 2023 02:40:44 -0700 Subject: [PATCH 12/12] v0.26.0 --- CREDITS | 4 ++++ Cargo.lock | 2 +- Cargo.toml | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CREDITS b/CREDITS index 0d7d117..a026e0b 100644 --- a/CREDITS +++ b/CREDITS @@ -21,11 +21,13 @@ dbrennand <52419383+dbrennand@users.noreply.github.com> Diego Magdaleno <38844659+DiegoMagdaleno@users.noreply.github.com> Dyras Edward <101938856+EdwardLangdon@users.noreply.github.com> +elliot <75391956+ellieeet123@users.noreply.github.com> erdnaxe Esmail EL BoB FireMasterK <20838718+FireMasterK@users.noreply.github.com> George Roubos git-bruh +gmnsii <95436780+gmnsii@users.noreply.github.com> guaddy <67671414+guaddy@users.noreply.github.com> Harsh Mishra igna @@ -62,11 +64,13 @@ robrobinbin <> robrobinbin <8597693+robrobinbin@users.noreply.github.com> robrobinbin Ruben Elshof <15641671+rubenelshof@users.noreply.github.com> +Rupert Angermeier Scoder12 <34356756+Scoder12@users.noreply.github.com> Slayer <51095261+GhostSlayer@users.noreply.github.com> Soheb somini somoso +Spenser Black Spike <19519553+spikecodes@users.noreply.github.com> spikecodes <19519553+spikecodes@users.noreply.github.com> sybenx diff --git a/Cargo.lock b/Cargo.lock index e947b07..287700c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -664,7 +664,7 @@ dependencies = [ [[package]] name = "libreddit" -version = "0.25.3" +version = "0.26.0" dependencies = [ "askama", "async-recursion", diff --git a/Cargo.toml b/Cargo.toml index 1375851..680e7ad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ name = "libreddit" description = " Alternative private front-end to Reddit" license = "AGPL-3.0" repository = "https://github.com/spikecodes/libreddit" -version = "0.25.3" +version = "0.26.0" authors = ["spikecodes <19519553+spikecodes@users.noreply.github.com>"] edition = "2021"