From a49d399f72d334adb5307eda2d23fbfa14d258be Mon Sep 17 00:00:00 2001 From: Spenser Black Date: Sun, 1 Jan 2023 13:38:52 -0500 Subject: [PATCH 1/6] 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 c15f305be0c97730f91694146c493fdccdc3371e Mon Sep 17 00:00:00 2001 From: Daniel Valentine Date: Sun, 1 Jan 2023 23:54:35 -0700 Subject: [PATCH 2/6] 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 3/6] 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 4/6] 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" From 5b06a3fc648ad9ed4f7db7a5d59b1af909d2c03d Mon Sep 17 00:00:00 2001 From: Matthew E Date: Tue, 3 Jan 2023 04:55:22 -0500 Subject: [PATCH 5/6] Add config system to read from file (#664) Co-authored-by: Daniel Valentine --- Cargo.lock | 163 ++++++++++++++++++++++++++++++++++++++------------ Cargo.toml | 2 + README.md | 7 +++ src/config.rs | 130 ++++++++++++++++++++++++++++++++++++++++ src/main.rs | 1 + src/utils.rs | 12 ++-- 6 files changed, 272 insertions(+), 43 deletions(-) create mode 100644 src/config.rs diff --git a/Cargo.lock b/Cargo.lock index 287700c..53d7bf9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10,9 +10,9 @@ checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" [[package]] name = "aho-corasick" -version = "0.7.19" +version = "0.7.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4f55bd91a0978cbfd91c457a164bab8b4001c833b7f323132c0a4e1922dd44e" +checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" dependencies = [ "memchr", ] @@ -88,9 +88,9 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.58" +version = "0.1.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e805d94e6b5001b651426cf4cd446b1ab5f319d27bab5c644f61de0a804360c" +checksum = "31e6e93155431f3931513b243d371981bb2770112b370c82745a1d19d2f99364" dependencies = [ "proc-macro2", "quote", @@ -168,9 +168,9 @@ checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba" [[package]] name = "bytes" -version = "1.2.1" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db" +checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c" [[package]] name = "cached" @@ -211,9 +211,9 @@ checksum = "3a4f925191b4367301851c6d99b09890311d74b0d43f274c0b34c86d308a3663" [[package]] name = "cc" -version = "1.0.76" +version = "1.0.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76a284da2e6fe2092f2353e51713435363112dfd60030e22add80be333fb928f" +checksum = "e9f73505338f7d905b19d18738976aae232eb46b8efc15554ffc56deb5d9ebe4" [[package]] name = "cfg-if" @@ -223,9 +223,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clap" -version = "4.0.24" +version = "4.0.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60494cedb60cb47462c0ff7be53de32c0e42a6fc2c772184554fa12bd9489c03" +checksum = "4d63b9e9c07271b9957ad22c173bae2a4d9a81127680962039296abcd2f8251d" dependencies = [ "bitflags", "clap_lex", @@ -331,9 +331,9 @@ dependencies = [ [[package]] name = "digest" -version = "0.10.5" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adfbc57365a37acbd2ebf2b64d7e69bb766e2fea813521ed536f5d0520dcf86c" +checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" dependencies = [ "block-buffer", "crypto-common", @@ -363,6 +363,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs_extra" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2022715d62ab30faffd124d40b76f4134a550a87792276512b18d63272333394" + [[package]] name = "futures" version = "0.3.25" @@ -567,9 +573,9 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.23.0" +version = "0.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d87c48c02e0dc5e3b849a2041db3029fd066650f8f717c07bf8ed78ccb895cac" +checksum = "1788965e61b367cd03a62950836d5cd41560c3577d90e40e0819373194d1661c" dependencies = [ "http", "hyper", @@ -598,9 +604,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "1.9.1" +version = "1.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" +checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" dependencies = [ "autocfg", "hashbrown", @@ -638,9 +644,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.137" +version = "0.2.138" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89" +checksum = "db6d7e329c562c5dfab7a46a2afabc8b987ab9a4834c9d1ca04dc54c1546cef8" [[package]] name = "libflate" @@ -682,10 +688,12 @@ dependencies = [ "regex", "route-recognizer", "rust-embed", + "sealed_test", "serde", "serde_json", "time", "tokio", + "toml", "url", ] @@ -792,9 +800,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "os_str_bytes" -version = "6.4.0" +version = "6.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5bf27447411e9ee3ff51186bf7a08e16c341efdde93f4d823e8844429bed7e" +checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee" [[package]] name = "parking" @@ -814,9 +822,9 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dc9e0dc2adc1c69d09143aff38d3d30c5c3f0df0dad82e6d25547af174ebec0" +checksum = "7ff9f3fef3968a3ec5945535ed654cb38ff72d7495a25619e2247fb15a2ed9ba" dependencies = [ "cfg-if", "libc", @@ -858,6 +866,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + [[package]] name = "quote" version = "1.0.21" @@ -923,6 +937,15 @@ version = "0.6.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + [[package]] name = "ring" version = "0.16.20" @@ -1018,6 +1041,18 @@ dependencies = [ "base64", ] +[[package]] +name = "rusty-forkfork" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ce85af4dfa2fb0c0143121ab5e424c71ea693867357c9159b8777b59984c218" +dependencies = [ + "fnv", + "quick-error", + "tempfile", + "wait-timeout", +] + [[package]] name = "ryu" version = "1.0.11" @@ -1059,6 +1094,28 @@ dependencies = [ "untrusted", ] +[[package]] +name = "sealed_test" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a608d94641cc17fe203b102db2ae86d47a236630192f0244ddbbbb0044c0272" +dependencies = [ + "fs_extra", + "rusty-forkfork", + "sealed_test_derive", + "tempfile", +] + +[[package]] +name = "sealed_test_derive" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b672e005ae58fef5da619d90b9f1c5b44b061890f4a371b3c96257a8a15e697" +dependencies = [ + "quote", + "syn", +] + [[package]] name = "security-framework" version = "2.7.0" @@ -1084,18 +1141,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.147" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d193d69bae983fc11a79df82342761dfbf28a99fc8d203dca4c3c1b590948965" +checksum = "256b9932320c590e707b94576e3cc1f7c9024d0ee6612dfbcf1cb106cbe8e055" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.147" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f1d362ca8fc9c3e3a7484440752472d68a6caa98f1ab81d99b5dfe517cec852" +checksum = "b4eae9b04cbffdfd550eb462ed33bc6a1b68c935127d008b27444d08380f94e4" dependencies = [ "proc-macro2", "quote", @@ -1104,9 +1161,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.87" +version = "1.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce777b7b150d76b9cf60d28b55f5847135a003f7d7350c6be7a773508ce7d45" +checksum = "020ff22c755c2ed3f8cf162dbb41a7268d934702f3ed3631656ea597e08fc3db" dependencies = [ "itoa", "ryu", @@ -1172,15 +1229,29 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "syn" -version = "1.0.103" +version = "1.0.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a864042229133ada95abf3b54fdc62ef5ccabe9515b64717bcb9a1919e59445d" +checksum = "60b9b43d45702de4c839cb9b51d9f529c5dd26a4aff255b42b1ebc03e88ee908" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] +[[package]] +name = "tempfile" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" +dependencies = [ + "cfg-if", + "fastrand", + "libc", + "redox_syscall", + "remove_dir_all", + "winapi", +] + [[package]] name = "thiserror" version = "1.0.37" @@ -1245,9 +1316,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.21.2" +version = "1.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e03c497dc955702ba729190dc4aac6f2a0ce97f913e5b1b5912fc5039d9099" +checksum = "eab6d665857cc6ca78d6e80303a02cea7a7851e85dfbd77cbdc09bd129f1ef46" dependencies = [ "autocfg", "bytes", @@ -1260,14 +1331,14 @@ dependencies = [ "signal-hook-registry", "socket2", "tokio-macros", - "winapi", + "windows-sys 0.42.0", ] [[package]] name = "tokio-macros" -version = "1.8.0" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484" +checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8" dependencies = [ "proc-macro2", "quote", @@ -1299,6 +1370,15 @@ dependencies = [ "tracing", ] +[[package]] +name = "toml" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7" +dependencies = [ + "serde", +] + [[package]] name = "tower-service" version = "0.3.2" @@ -1333,9 +1413,9 @@ checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" [[package]] name = "typenum" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" +checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" [[package]] name = "unicase" @@ -1390,6 +1470,15 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "wait-timeout" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" +dependencies = [ + "libc", +] + [[package]] name = "waker-fn" version = "1.1.0" diff --git a/Cargo.toml b/Cargo.toml index 680e7ad..8d90338 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,10 +27,12 @@ url = "2.3.1" rust-embed = { version = "6.4.2", features = ["include-exclude"] } libflate = "1.2.0" brotli = { version = "3.3.4", features = ["std"] } +toml = "0.5.9" once_cell = "1.16.0" [dev-dependencies] lipsum = "0.8.2" +sealed_test = "1.0.0" [profile.release] codegen-units = 1 diff --git a/README.md b/README.md index 5f3c647..7386272 100644 --- a/README.md +++ b/README.md @@ -208,6 +208,13 @@ Assign a default value for each user-modifiable setting by passing environment v | `HIDE_HLS_NOTIFICATION` | `["on", "off"]` | `off` | | `AUTOPLAY_VIDEOS` | `["on", "off"]` | `off` | +You can also configure Libreddit with a configuration file. An example `libreddit.toml` can be found below: + +```toml +LIBREDDIT_DEFAULT_WIDE = "on" +LIBREDDIT_DEFAULT_USE_HLS = "on" +``` + ### Examples ```bash diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..c2d2055 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,130 @@ +use once_cell::sync::Lazy; +use std::{env::var, fs::read_to_string}; + +// Waiting for https://github.com/rust-lang/rust/issues/74465 to land, so we +// can reduce reliance on once_cell. +// +// This is the local static that is initialized at runtime (technically at +// first request) and contains the instance settings. +static CONFIG: Lazy = Lazy::new(Config::load); + +/// Stores the configuration parsed from the environment variables and the +/// config file. `Config::Default()` contains None for each setting. +#[derive(Default, serde::Deserialize)] +pub struct Config { + #[serde(rename = "LIBREDDIT_SFW_ONLY")] + sfw_only: Option, + + #[serde(rename = "LIBREDDIT_DEFAULT_THEME")] + default_theme: Option, + + #[serde(rename = "LIBREDDIT_DEFAULT_FRONT_PAGE")] + default_front_page: Option, + + #[serde(rename = "LIBREDDIT_DEFAULT_LAYOUT")] + default_layout: Option, + + #[serde(rename = "LIBREDDIT_DEFAULT_WIDE")] + default_wide: Option, + + #[serde(rename = "LIBREDDIT_DEFAULT_COMMENT_SORT")] + default_comment_sort: Option, + + #[serde(rename = "LIBREDDIT_DEFAULT_POST_SORT")] + default_post_sort: Option, + + #[serde(rename = "LIBREDDIT_DEFAULT_SHOW_NSFW")] + default_show_nsfw: Option, + + #[serde(rename = "LIBREDDIT_DEFAULT_BLUR_NSFW")] + default_blur_nsfw: Option, + + #[serde(rename = "LIBREDDIT_DEFAULT_USE_HLS")] + default_use_hls: Option, + + #[serde(rename = "LIBREDDIT_DEFAULT_HIDE_HLS_NOTIFICATION")] + default_hide_hls_notification: Option, +} + +impl Config { + /// Load the configuration from the environment variables and the config file. + /// In the case that there are no environment variables set and there is no + /// config file, this function returns a Config that contains all None values. + pub fn load() -> Self { + // Read from libreddit.toml config file. If for any reason, it fails, the + // default `Config` is used (all None values) + let config: Config = toml::from_str(&read_to_string("libreddit.toml").unwrap_or_default()).unwrap_or_default(); + // This function defines the order of preference - first check for + // environment variables with "LIBREDDIT", then check the config, then if + // both are `None`, return a `None` via the `map_or_else` function + let parse = |key: &str| -> Option { var(key).ok().map_or_else(|| get_setting_from_config(key, &config), Some) }; + Self { + sfw_only: parse("LIBREDDIT_SFW_ONLY"), + default_theme: parse("LIBREDDIT_DEFAULT_THEME"), + default_front_page: parse("LIBREDDIT_DEFAULT_FRONT_PAGE"), + default_layout: parse("LIBREDDIT_DEFAULT_LAYOUT"), + default_post_sort: parse("LIBREDDIT_DEFAULT_POST_SORT"), + default_wide: parse("LIBREDDIT_DEFAULT_WIDE"), + default_comment_sort: parse("LIBREDDIT_DEFAULT_COMMENT_SORT"), + default_show_nsfw: parse("LIBREDDIT_DEFAULT_SHOW_NSFW"), + default_blur_nsfw: parse("LIBREDDIT_DEFAULT_BLUR_NSFW"), + default_use_hls: parse("LIBREDDIT_DEFAULT_USE_HLS"), + default_hide_hls_notification: parse("LIBREDDIT_DEFAULT_HIDE_HLS"), + } + } +} + +fn get_setting_from_config(name: &str, config: &Config) -> Option { + match name { + "LIBREDDIT_SFW_ONLY" => config.sfw_only.clone(), + "LIBREDDIT_DEFAULT_THEME" => config.default_theme.clone(), + "LIBREDDIT_DEFAULT_FRONT_PAGE" => config.default_front_page.clone(), + "LIBREDDIT_DEFAULT_LAYOUT" => config.default_layout.clone(), + "LIBREDDIT_DEFAULT_COMMENT_SORT" => config.default_comment_sort.clone(), + "LIBREDDIT_DEFAULT_POST_SORT" => config.default_post_sort.clone(), + "LIBREDDIT_DEFAULT_SHOW_NSFW" => config.default_show_nsfw.clone(), + "LIBREDDIT_DEFAULT_BLUR_NSFW" => config.default_blur_nsfw.clone(), + "LIBREDDIT_DEFAULT_USE_HLS" => config.default_use_hls.clone(), + "LIBREDDIT_DEFAULT_HIDE_HLS_NOTIFICATION" => config.default_hide_hls_notification.clone(), + "LIBREDDIT_DEFAULT_WIDE" => config.default_wide.clone(), + _ => None, + } +} + +/// Retrieves setting from environment variable or config file. +pub(crate) fn get_setting(name: &str) -> Option { + get_setting_from_config(name, &CONFIG) +} + +#[cfg(test)] +use {sealed_test::prelude::*, std::fs::write}; + +#[test] +#[sealed_test(env = [("LIBREDDIT_SFW_ONLY", "1")])] +fn test_env_var() { + assert!(crate::utils::sfw_only()) +} + +#[test] +#[sealed_test] +fn test_config() { + let config_to_write = r#"LIBREDDIT_DEFAULT_COMMENT_SORT = "best""#; + write("libreddit.toml", config_to_write).unwrap(); + assert_eq!(get_setting("LIBREDDIT_DEFAULT_COMMENT_SORT"), Some("best".into())); +} + +#[test] +#[sealed_test(env = [("LIBREDDIT_DEFAULT_COMMENT_SORT", "top")])] +fn test_env_config_precedence() { + let config_to_write = r#"LIBREDDIT_DEFAULT_COMMENT_SORT = "best""#; + write("libreddit.toml", config_to_write).unwrap(); + assert_eq!(get_setting("LIBREDDIT_DEFAULT_COMMENT_SORT"), Some("top".into())) +} + +#[test] +#[sealed_test(env = [("LIBREDDIT_DEFAULT_COMMENT_SORT", "top")])] +fn test_alt_env_config_precedence() { + let config_to_write = r#"LIBREDDIT_DEFAULT_COMMENT_SORT = "best""#; + write("libreddit.toml", config_to_write).unwrap(); + assert_eq!(get_setting("LIBREDDIT_DEFAULT_COMMENT_SORT"), Some("top".into())) +} diff --git a/src/main.rs b/src/main.rs index 25c2aea..bffe99c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,7 @@ #![allow(clippy::cmp_owned)] // Reference local files +mod config; mod duplicates; mod post; mod search; diff --git a/src/utils.rs b/src/utils.rs index fee97e9..8c8d273 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -680,8 +680,8 @@ pub fn setting(req: &Request, name: &str) -> String { req .cookie(name) .unwrap_or_else(|| { - // If there is no cookie for this setting, try receiving a default from an environment variable - if let Ok(default) = std::env::var(format!("LIBREDDIT_DEFAULT_{}", name.to_uppercase())) { + // If there is no cookie for this setting, try receiving a default from the config + if let Some(default) = crate::config::get_setting(&format!("LIBREDDIT_DEFAULT_{}", name.to_uppercase())) { Cookie::new(name, default) } else { Cookie::named(name) @@ -866,7 +866,7 @@ pub async fn error(req: Request, msg: impl ToString) -> Result, msg: impl ToString) -> Result bool { - match env::var("LIBREDDIT_SFW_ONLY") { - Ok(val) => val == "on", - Err(_) => false, + match crate::config::get_setting("LIBREDDIT_SFW_ONLY") { + Some(val) => val == "on", + None => false, } } From 050eaedf15d2fd611f4c623da2629f22e5cc36cd Mon Sep 17 00:00:00 2001 From: Tokarak <63452145+Tokarak@users.noreply.github.com> Date: Tue, 3 Jan 2023 14:56:17 +0000 Subject: [PATCH 6/6] Remove unused dep "async-recursion" Found using cargo-udeps. Checked. --- Cargo.lock | 12 ------------ Cargo.toml | 1 - 2 files changed, 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 53d7bf9..a3865f1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -75,17 +75,6 @@ dependencies = [ "syn", ] -[[package]] -name = "async-recursion" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cda8f4bcc10624c4e85bc66b3f452cca98cfa5ca002dc83a16aad2367641bea" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "async-trait" version = "0.1.59" @@ -673,7 +662,6 @@ name = "libreddit" version = "0.26.0" dependencies = [ "askama", - "async-recursion", "brotli", "cached", "clap", diff --git a/Cargo.toml b/Cargo.toml index 8d90338..3554806 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,6 @@ edition = "2021" [dependencies] askama = { version = "0.11.1", default-features = false } -async-recursion = "1.0.0" cached = "0.40.0" clap = { version = "4.0.24", default-features = false, features = ["std"] } regex = "1.7.0"