Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add RSS feeds (fix #57) #90

Merged
merged 7 commits into from
Jul 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
234 changes: 218 additions & 16 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,10 @@ fastrand = "2.0.1"
log = "0.4.20"
pretty_env_logger = "0.5.0"
dotenvy = "0.15.7"
rss = "2.0.7"
arc-swap = "1.7.1"


[dev-dependencies]
lipsum = "0.9.0"
sealed_test = "1.0.0"
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -381,7 +381,8 @@ Assign a default value for each instance-specific setting by passing environment
| `ROBOTS_DISABLE_INDEXING` | `["on", "off"]` | `off` | Disables indexing of the instance by search engines. |
| `PUSHSHIFT_FRONTEND` | String | `undelete.pullpush.io` | Allows the server to set the Pushshift frontend to be used with "removed" links. |
| `PORT` | Integer 0-65535 | `8080` | The **internal** port Redlib listens on. |

| `ENABLE_RSS` | `["on", "off"]` | `off` | Enables RSS feed generation. |
| `FULL_URL` | String | (empty) | Allows for proper URLs (for now, only needed by RSS)
## Default user settings

Assign a default value for each user-modifiable setting by passing environment variables to Redlib in the format `REDLIB_DEFAULT_{Y}`. Replace `{Y}` with the setting name (see list below) in capital letters.
Expand Down
10 changes: 8 additions & 2 deletions app.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@
"REDLIB_BANNER": {
"required": false
},
"REDLIB_ROBOTS_DISABLE_INDEXING": {
"required": false
"REDLIB_ROBOTS_DISABLE_INDEXING": {
"required": false
},
"REDLIB_DEFAULT_SUBSCRIPTIONS": {
"required": false
Expand All @@ -70,6 +70,12 @@
},
"REDLIB_PUSHSHIFT_FRONTEND": {
"required": false
},
"REDLIB_ENABLE_RSS": {
"required": false
},
"REDLIB_FULL_URL": {
"required": false
}
}
}
10 changes: 10 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,12 @@ pub struct Config {
#[serde(rename = "REDLIB_PUSHSHIFT_FRONTEND")]
#[serde(alias = "LIBREDDIT_PUSHSHIFT_FRONTEND")]
pub(crate) pushshift: Option<String>,

#[serde(rename = "REDLIB_ENABLE_RSS")]
pub(crate) enable_rss: Option<String>,

#[serde(rename = "REDLIB_FULL_URL")]
pub(crate) full_url: Option<String>,
}

impl Config {
Expand Down Expand Up @@ -148,6 +154,8 @@ impl Config {
banner: parse("REDLIB_BANNER"),
robots_disable_indexing: parse("REDLIB_ROBOTS_DISABLE_INDEXING"),
pushshift: parse("REDLIB_PUSHSHIFT_FRONTEND"),
enable_rss: parse("REDLIB_ENABLE_RSS"),
full_url: parse("REDLIB_FULL_URL"),
}
}
}
Expand Down Expand Up @@ -175,6 +183,8 @@ fn get_setting_from_config(name: &str, config: &Config) -> Option<String> {
"REDLIB_BANNER" => config.banner.clone(),
"REDLIB_ROBOTS_DISABLE_INDEXING" => config.robots_disable_indexing.clone(),
"REDLIB_PUSHSHIFT_FRONTEND" => config.pushshift.clone(),
"REDLIB_ENABLE_RSS" => config.enable_rss.clone(),
"REDLIB_FULL_URL" => config.full_url.clone(),
_ => None,
}
}
Expand Down
6 changes: 6 additions & 0 deletions src/instance_info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,8 @@ impl InstanceInfo {
["Compile mode", &self.compile_mode],
["SFW only", &convert(&self.config.sfw_only)],
["Pushshift frontend", &convert(&self.config.pushshift)],
["RSS enabled", &convert(&self.config.enable_rss)],
["Full URL", &convert(&self.config.full_url)],
//TODO: fallback to crate::config::DEFAULT_PUSHSHIFT_FRONTEND
])
.with_header_row(["Settings"]),
Expand Down Expand Up @@ -165,6 +167,8 @@ impl InstanceInfo {
Compile mode: {}\n
SFW only: {:?}\n
Pushshift frontend: {:?}\n
RSS enabled: {:?}\n
Full URL: {:?}\n
Config:\n
Banner: {:?}\n
Hide awards: {:?}\n
Expand All @@ -189,6 +193,8 @@ impl InstanceInfo {
self.deploy_unix_ts,
self.compile_mode,
self.config.sfw_only,
self.config.enable_rss,
self.config.full_url,
self.config.pushshift,
self.config.banner,
self.config.default_hide_awards,
Expand Down
4 changes: 4 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,7 @@ async fn main() {
app.at("/u/:name/comments/:id/:title/:comment_id").get(|r| post::item(r).boxed());

app.at("/user/[deleted]").get(|req| error(req, "User has deleted their account").boxed());
app.at("/user/:name.rss").get(|r| user::rss(r).boxed());
app.at("/user/:name").get(|r| user::profile(r).boxed());
app.at("/user/:name/:listing").get(|r| user::profile(r).boxed());
app.at("/user/:name/comments/:id").get(|r| post::item(r).boxed());
Expand All @@ -265,6 +266,9 @@ async fn main() {
app.at("/settings/restore").get(|r| settings::restore(r).boxed());
app.at("/settings/update").get(|r| settings::update(r).boxed());

// RSS Subscriptions
app.at("/r/:sub.rss").get(|r| subreddit::rss(r).boxed());

// Subreddit services
app
.at("/r/:sub")
Expand Down
51 changes: 51 additions & 0 deletions src/subreddit.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::{config, utils};
// CRATES
use crate::utils::{
catch_random, error, filter_posts, format_num, format_url, get_filters, nsfw_landing, param, redirect, rewrite_urls, setting, template, val, Post, Preferences, Subreddit,
Expand Down Expand Up @@ -459,6 +460,56 @@ async fn subreddit(sub: &str, quarantined: bool) -> Result<Subreddit, String> {
})
}

pub async fn rss(req: Request<Body>) -> Result<Response<Body>, String> {
if config::get_setting("REDLIB_ENABLE_RSS").is_none() {
return Ok(error(req, "RSS is disabled on this instance.").await.unwrap_or_default());
}

use hyper::header::CONTENT_TYPE;
use rss::{ChannelBuilder, Item};

// Get subreddit
let sub = req.param("sub").unwrap_or_default();
let post_sort = req.cookie("post_sort").map_or_else(|| "hot".to_string(), |c| c.value().to_string());
let sort = req.param("sort").unwrap_or_else(|| req.param("id").unwrap_or(post_sort));

// Get path
let path = format!("/r/{sub}/{sort}.json?{}", req.uri().query().unwrap_or_default());

// Get subreddit data
let subreddit = subreddit(&sub, false).await?;

// Get posts
let (posts, _) = Post::fetch(&path, false).await?;

// Build the RSS feed
let channel = ChannelBuilder::default()
.title(&subreddit.title)
.description(&subreddit.description)
.items(
posts
.into_iter()
.map(|post| Item {
title: Some(post.title.to_string()),
link: Some(utils::get_post_url(&post)),
author: Some(post.author.name),
content: Some(rewrite_urls(&post.body)),
..Default::default()
})
.collect::<Vec<_>>(),
)
.build();

// Serialize the feed to RSS
let body = channel.to_string().into_bytes();

// Create the HTTP response
let mut res = Response::new(Body::from(body));
res.headers_mut().insert(CONTENT_TYPE, hyper::header::HeaderValue::from_static("application/rss+xml"));

Ok(res)
}

#[tokio::test(flavor = "multi_thread")]
async fn test_fetching_subreddit() {
let subreddit = subreddit("rust", false).await;
Expand Down
51 changes: 51 additions & 0 deletions src/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
use crate::client::json;
use crate::server::RequestExt;
use crate::utils::{error, filter_posts, format_url, get_filters, nsfw_landing, param, setting, template, Post, Preferences, User};
use crate::{config, utils};
use askama::Template;
use hyper::{Body, Request, Response};
use time::{macros::format_description, OffsetDateTime};
Expand Down Expand Up @@ -129,6 +130,56 @@ async fn user(name: &str) -> Result<User, String> {
})
}

pub async fn rss(req: Request<Body>) -> Result<Response<Body>, String> {
if config::get_setting("REDLIB_ENABLE_RSS").is_none() {
return Ok(error(req, "RSS is disabled on this instance.").await.unwrap_or_default());
}
use crate::utils::rewrite_urls;
use hyper::header::CONTENT_TYPE;
use rss::{ChannelBuilder, Item};

// Get user
let user_str = req.param("name").unwrap_or_default();

let listing = req.param("listing").unwrap_or_else(|| "overview".to_string());

// Get path
let path = format!("/user/{user_str}/{listing}.json?{}&raw_json=1", req.uri().query().unwrap_or_default(),);

// Get user
let user_obj = user(&user_str).await.unwrap_or_default();

// Get posts
let (posts, _) = Post::fetch(&path, false).await?;

// Build the RSS feed
let channel = ChannelBuilder::default()
.title(user_str)
.description(user_obj.description)
.items(
posts
.into_iter()
.map(|post| Item {
title: Some(post.title.to_string()),
link: Some(utils::get_post_url(&post)),
author: Some(post.author.name),
content: Some(rewrite_urls(&post.body)),
..Default::default()
})
.collect::<Vec<_>>(),
)
.build();

// Serialize the feed to RSS
let body = channel.to_string().into_bytes();

// Create the HTTP response
let mut res = Response::new(Body::from(body));
res.headers_mut().insert(CONTENT_TYPE, hyper::header::HeaderValue::from_static("application/rss+xml"));

Ok(res)
}

#[tokio::test(flavor = "multi_thread")]
async fn test_fetching_user() {
let user = user("spez").await;
Expand Down
30 changes: 29 additions & 1 deletion src/utils.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#![allow(dead_code)]
use crate::config::get_setting;
use crate::config::{self, get_setting};
//
// CRATES
//
Expand All @@ -15,6 +15,7 @@ use serde_json::Value;
use std::collections::{HashMap, HashSet};
use std::env;
use std::str::FromStr;
use std::string::ToString;
use time::{macros::format_description, Duration, OffsetDateTime};
use url::Url;

Expand Down Expand Up @@ -327,6 +328,7 @@ pub struct Post {
pub gallery: Vec<GalleryMedia>,
pub awards: Awards,
pub nsfw: bool,
pub out_url: Option<String>,
pub ws_url: String,
}

Expand Down Expand Up @@ -435,6 +437,7 @@ impl Post {
awards,
nsfw: post["data"]["over_18"].as_bool().unwrap_or_default(),
ws_url: val(post, "websocket_url"),
out_url: post["data"]["url_overridden_by_dest"].as_str().map(|a| a.to_string()),
});
}
Ok((posts, res["data"]["after"].as_str().unwrap_or_default().to_string()))
Expand Down Expand Up @@ -770,6 +773,7 @@ pub async fn parse_post(post: &Value) -> Post {
awards,
nsfw: post["data"]["over_18"].as_bool().unwrap_or_default(),
ws_url: val(post, "websocket_url"),
out_url: post["data"]["url_overridden_by_dest"].as_str().map(|a| a.to_string()),
}
}

Expand Down Expand Up @@ -1082,6 +1086,16 @@ pub fn sfw_only() -> bool {
}
}

/// Returns true if the config/env variable REDLIB_ENABLE_RSS is set to "on".
/// If this variable is set as such, the instance will enable RSS feeds.
/// Otherwise, the instance will not provide RSS feeds.
pub fn enable_rss() -> bool {
match get_setting("REDLIB_ENABLE_RSS") {
Some(val) => val == "on",
None => false,
}
}

// Determines if a request shoud redirect to a nsfw landing gate.
pub fn should_be_nsfw_gated(req: &Request<Body>, req_url: &str) -> bool {
let sfw_instance = sfw_only();
Expand Down Expand Up @@ -1137,6 +1151,20 @@ pub fn url_path_basename(path: &str) -> String {
}
}

// Returns the URL of a post, as needed by RSS feeds
pub fn get_post_url(post: &Post) -> String {
if let Some(out_url) = &post.out_url {
// Handle cross post
if out_url.starts_with("/r/") {
format!("{}{}", config::get_setting("REDLIB_FULL_URL").unwrap_or_default(), out_url)
} else {
out_url.to_string()
}
} else {
format!("{}{}", config::get_setting("REDLIB_FULL_URL").unwrap_or_default(), post.permalink)
}
}

#[cfg(test)]
mod tests {
use super::{format_num, format_url, rewrite_urls};
Expand Down
Loading
Loading