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() %}
+
+ {% 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 %}
+
+
+ 😱
+ {% if res_type == crate::utils::ResourceType::Subreddit %}
+ r/{{ res }} is a NSFW community!
+ {% else if res_type == crate::utils::ResourceType::User %}
+ u/{{ res }}'s content is NSFW!
+ {% else if res_type == crate::utils::ResourceType::Post %}
+ This post is NSFW!
+ {% endif %}
+
+
+
+
+ {% if crate::utils::sfw_only() %}
+ This instance of Libreddit is SFW-only.
+ {% else %}
+ Enable "Show NSFW posts" in settings to view this {% if res_type == crate::utils::ResourceType::Subreddit %}subreddit{% else if res_type == crate::utils::ResourceType::User %}user's posts or comments{% else if res_type == crate::utils::ResourceType::Post %}post{% endif %}.
+ {% endif %}
+
+