Add RSS feeds (fix #57) (#90)

* Add RSS feeds

* feat(rss): feature-ify rss

* feat(rss): config-ify rss

* fix(rss): update info page

* feat(rss): conditionally add RSS feeds to user and sub pages

* feat(rss): implement URLs for RSS
This commit is contained in:
Matthew Esposito 2024-07-21 14:09:34 -04:00 committed by GitHub
parent 9bdb5c8966
commit 374238abc3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 1808 additions and 1185 deletions

234
Cargo.lock generated
View File

@ -98,7 +98,7 @@ dependencies = [
"mime_guess", "mime_guess",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.68",
] ]
[[package]] [[package]]
@ -124,7 +124,20 @@ checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.68",
]
[[package]]
name = "atom_syndication"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2f34613907f31c9dbef0240156db3c9263f34842b6e1a8999d2304ea62c8a30"
dependencies = [
"chrono",
"derive_builder 0.20.0",
"diligent-date-parser",
"never",
"quick-xml 0.31.0",
] ]
[[package]] [[package]]
@ -236,10 +249,10 @@ version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "771aa57f3b17da6c8bcacb187bb9ec9bc81c8160e72342e67c329e0e1651a669" checksum = "771aa57f3b17da6c8bcacb187bb9ec9bc81c8160e72342e67c329e0e1651a669"
dependencies = [ dependencies = [
"darling", "darling 0.20.9",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.68",
] ]
[[package]] [[package]]
@ -260,6 +273,15 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chrono"
version = "0.4.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401"
dependencies = [
"num-traits",
]
[[package]] [[package]]
name = "clap" name = "clap"
version = "4.5.7" version = "4.5.7"
@ -348,14 +370,38 @@ dependencies = [
"typenum", "typenum",
] ]
[[package]]
name = "darling"
version = "0.14.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850"
dependencies = [
"darling_core 0.14.4",
"darling_macro 0.14.4",
]
[[package]] [[package]]
name = "darling" name = "darling"
version = "0.20.9" version = "0.20.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83b2eb4d90d12bdda5ed17de686c2acb4c57914f8f921b8da7e112b5a36f3fe1" checksum = "83b2eb4d90d12bdda5ed17de686c2acb4c57914f8f921b8da7e112b5a36f3fe1"
dependencies = [ dependencies = [
"darling_core", "darling_core 0.20.9",
"darling_macro", "darling_macro 0.20.9",
]
[[package]]
name = "darling_core"
version = "0.14.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0"
dependencies = [
"fnv",
"ident_case",
"proc-macro2",
"quote",
"strsim 0.10.0",
"syn 1.0.109",
] ]
[[package]] [[package]]
@ -368,8 +414,19 @@ dependencies = [
"ident_case", "ident_case",
"proc-macro2", "proc-macro2",
"quote", "quote",
"strsim", "strsim 0.11.1",
"syn", "syn 2.0.68",
]
[[package]]
name = "darling_macro"
version = "0.14.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e"
dependencies = [
"darling_core 0.14.4",
"quote",
"syn 1.0.109",
] ]
[[package]] [[package]]
@ -378,9 +435,9 @@ version = "0.20.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "733cabb43482b1a1b53eee8583c2b9e8684d592215ea83efd305dd31bc2f0178" checksum = "733cabb43482b1a1b53eee8583c2b9e8684d592215ea83efd305dd31bc2f0178"
dependencies = [ dependencies = [
"darling_core", "darling_core 0.20.9",
"quote", "quote",
"syn", "syn 2.0.68",
] ]
[[package]] [[package]]
@ -398,6 +455,68 @@ dependencies = [
"powerfmt", "powerfmt",
] ]
[[package]]
name = "derive_builder"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d67778784b508018359cbc8696edb3db78160bab2c2a28ba7f56ef6932997f8"
dependencies = [
"derive_builder_macro 0.12.0",
]
[[package]]
name = "derive_builder"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0350b5cb0331628a5916d6c5c0b72e97393b8b6b03b47a9284f4e7f5a405ffd7"
dependencies = [
"derive_builder_macro 0.20.0",
]
[[package]]
name = "derive_builder_core"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c11bdc11a0c47bc7d37d582b5285da6849c96681023680b906673c5707af7b0f"
dependencies = [
"darling 0.14.4",
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "derive_builder_core"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d48cda787f839151732d396ac69e3473923d54312c070ee21e9effcaa8ca0b1d"
dependencies = [
"darling 0.20.9",
"proc-macro2",
"quote",
"syn 2.0.68",
]
[[package]]
name = "derive_builder_macro"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebcda35c7a396850a55ffeac740804b40ffec779b98fffbb1738f4033f0ee79e"
dependencies = [
"derive_builder_core 0.12.0",
"syn 1.0.109",
]
[[package]]
name = "derive_builder_macro"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "206868b8242f27cecce124c19fd88157fbd0dd334df2587f36417bafbc85097b"
dependencies = [
"derive_builder_core 0.20.0",
"syn 2.0.68",
]
[[package]] [[package]]
name = "digest" name = "digest"
version = "0.10.7" version = "0.10.7"
@ -408,12 +527,30 @@ dependencies = [
"crypto-common", "crypto-common",
] ]
[[package]]
name = "diligent-date-parser"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6cf7fe294274a222363f84bcb63cdea762979a0443b4cf1f4f8fd17c86b1182"
dependencies = [
"chrono",
]
[[package]] [[package]]
name = "dotenvy" name = "dotenvy"
version = "0.15.7" version = "0.15.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
[[package]]
name = "encoding_rs"
version = "0.8.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1"
dependencies = [
"cfg-if",
]
[[package]] [[package]]
name = "env_logger" name = "env_logger"
version = "0.10.2" version = "0.10.2"
@ -862,6 +999,12 @@ dependencies = [
"windows-sys 0.48.0", "windows-sys 0.48.0",
] ]
[[package]]
name = "never"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c96aba5aa877601bb3f6dd6a63a969e1f82e60646e81e71b14496995e9853c91"
[[package]] [[package]]
name = "nom" name = "nom"
version = "7.1.3" version = "7.1.3"
@ -878,6 +1021,15 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]] [[package]]
name = "num_cpus" name = "num_cpus"
version = "1.16.0" version = "1.16.0"
@ -1002,6 +1154,26 @@ version = "1.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
[[package]]
name = "quick-xml"
version = "0.30.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eff6510e86862b57b210fd8cbe8ed3f0d7d600b9c2863cd4549a2e033c66e956"
dependencies = [
"encoding_rs",
"memchr",
]
[[package]]
name = "quick-xml"
version = "0.31.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33"
dependencies = [
"encoding_rs",
"memchr",
]
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.36" version = "1.0.36"
@ -1061,6 +1233,7 @@ dependencies = [
"pretty_env_logger", "pretty_env_logger",
"regex", "regex",
"route-recognizer", "route-recognizer",
"rss",
"rust-embed", "rust-embed",
"sealed_test", "sealed_test",
"serde", "serde",
@ -1138,6 +1311,18 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "afab94fb28594581f62d981211a9a4d53cc8130bbcbbb89a0440d9b8e81a7746" checksum = "afab94fb28594581f62d981211a9a4d53cc8130bbcbbb89a0440d9b8e81a7746"
[[package]]
name = "rss"
version = "2.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7b2c77eb4450d7d5f98df52c381cd6c4e19b75dad9209a9530b85a44510219a"
dependencies = [
"atom_syndication",
"derive_builder 0.12.0",
"never",
"quick-xml 0.30.0",
]
[[package]] [[package]]
name = "rust-embed" name = "rust-embed"
version = "8.4.0" version = "8.4.0"
@ -1158,7 +1343,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"rust-embed-utils", "rust-embed-utils",
"syn", "syn 2.0.68",
"walkdir", "walkdir",
] ]
@ -1307,7 +1492,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77253fb2d4451418d07025826028bcb96ee42d3e58859689a70ce62908009db6" checksum = "77253fb2d4451418d07025826028bcb96ee42d3e58859689a70ce62908009db6"
dependencies = [ dependencies = [
"quote", "quote",
"syn", "syn 2.0.68",
] ]
[[package]] [[package]]
@ -1350,7 +1535,7 @@ checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.68",
] ]
[[package]] [[package]]
@ -1437,6 +1622,12 @@ version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
[[package]]
name = "strsim"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]] [[package]]
name = "strsim" name = "strsim"
version = "0.11.1" version = "0.11.1"
@ -1449,6 +1640,17 @@ version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "syn"
version = "1.0.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.68" version = "2.0.68"
@ -1498,7 +1700,7 @@ checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.68",
] ]
[[package]] [[package]]
@ -1576,7 +1778,7 @@ checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.68",
] ]
[[package]] [[package]]
@ -1950,7 +2152,7 @@ checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.68",
] ]
[[package]] [[package]]

View File

@ -42,8 +42,10 @@ fastrand = "2.0.1"
log = "0.4.20" log = "0.4.20"
pretty_env_logger = "0.5.0" pretty_env_logger = "0.5.0"
dotenvy = "0.15.7" dotenvy = "0.15.7"
rss = "2.0.7"
arc-swap = "1.7.1" arc-swap = "1.7.1"
[dev-dependencies] [dev-dependencies]
lipsum = "0.9.0" lipsum = "0.9.0"
sealed_test = "1.0.0" sealed_test = "1.0.0"

View File

@ -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. | | `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. | | `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. | | `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 ## 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. 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.

View File

@ -56,8 +56,8 @@
"REDLIB_BANNER": { "REDLIB_BANNER": {
"required": false "required": false
}, },
"REDLIB_ROBOTS_DISABLE_INDEXING": { "REDLIB_ROBOTS_DISABLE_INDEXING": {
"required": false "required": false
}, },
"REDLIB_DEFAULT_SUBSCRIPTIONS": { "REDLIB_DEFAULT_SUBSCRIPTIONS": {
"required": false "required": false
@ -70,6 +70,12 @@
}, },
"REDLIB_PUSHSHIFT_FRONTEND": { "REDLIB_PUSHSHIFT_FRONTEND": {
"required": false "required": false
},
"REDLIB_ENABLE_RSS": {
"required": false
},
"REDLIB_FULL_URL": {
"required": false
} }
} }
} }

View File

@ -103,6 +103,12 @@ pub struct Config {
#[serde(rename = "REDLIB_PUSHSHIFT_FRONTEND")] #[serde(rename = "REDLIB_PUSHSHIFT_FRONTEND")]
#[serde(alias = "LIBREDDIT_PUSHSHIFT_FRONTEND")] #[serde(alias = "LIBREDDIT_PUSHSHIFT_FRONTEND")]
pub(crate) pushshift: Option<String>, 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 { impl Config {
@ -148,6 +154,8 @@ impl Config {
banner: parse("REDLIB_BANNER"), banner: parse("REDLIB_BANNER"),
robots_disable_indexing: parse("REDLIB_ROBOTS_DISABLE_INDEXING"), robots_disable_indexing: parse("REDLIB_ROBOTS_DISABLE_INDEXING"),
pushshift: parse("REDLIB_PUSHSHIFT_FRONTEND"), pushshift: parse("REDLIB_PUSHSHIFT_FRONTEND"),
enable_rss: parse("REDLIB_ENABLE_RSS"),
full_url: parse("REDLIB_FULL_URL"),
} }
} }
} }
@ -175,6 +183,8 @@ fn get_setting_from_config(name: &str, config: &Config) -> Option<String> {
"REDLIB_BANNER" => config.banner.clone(), "REDLIB_BANNER" => config.banner.clone(),
"REDLIB_ROBOTS_DISABLE_INDEXING" => config.robots_disable_indexing.clone(), "REDLIB_ROBOTS_DISABLE_INDEXING" => config.robots_disable_indexing.clone(),
"REDLIB_PUSHSHIFT_FRONTEND" => config.pushshift.clone(), "REDLIB_PUSHSHIFT_FRONTEND" => config.pushshift.clone(),
"REDLIB_ENABLE_RSS" => config.enable_rss.clone(),
"REDLIB_FULL_URL" => config.full_url.clone(),
_ => None, _ => None,
} }
} }

View File

@ -126,6 +126,8 @@ impl InstanceInfo {
["Compile mode", &self.compile_mode], ["Compile mode", &self.compile_mode],
["SFW only", &convert(&self.config.sfw_only)], ["SFW only", &convert(&self.config.sfw_only)],
["Pushshift frontend", &convert(&self.config.pushshift)], ["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 //TODO: fallback to crate::config::DEFAULT_PUSHSHIFT_FRONTEND
]) ])
.with_header_row(["Settings"]), .with_header_row(["Settings"]),
@ -165,6 +167,8 @@ impl InstanceInfo {
Compile mode: {}\n Compile mode: {}\n
SFW only: {:?}\n SFW only: {:?}\n
Pushshift frontend: {:?}\n Pushshift frontend: {:?}\n
RSS enabled: {:?}\n
Full URL: {:?}\n
Config:\n Config:\n
Banner: {:?}\n Banner: {:?}\n
Hide awards: {:?}\n Hide awards: {:?}\n
@ -189,6 +193,8 @@ impl InstanceInfo {
self.deploy_unix_ts, self.deploy_unix_ts,
self.compile_mode, self.compile_mode,
self.config.sfw_only, self.config.sfw_only,
self.config.enable_rss,
self.config.full_url,
self.config.pushshift, self.config.pushshift,
self.config.banner, self.config.banner,
self.config.default_hide_awards, self.config.default_hide_awards,

View File

@ -254,6 +254,7 @@ async fn main() {
app.at("/u/:name/comments/:id/:title/:comment_id").get(|r| post::item(r).boxed()); 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/[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").get(|r| user::profile(r).boxed());
app.at("/user/:name/:listing").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()); app.at("/user/:name/comments/:id").get(|r| post::item(r).boxed());
@ -265,6 +266,9 @@ async fn main() {
app.at("/settings/restore").get(|r| settings::restore(r).boxed()); app.at("/settings/restore").get(|r| settings::restore(r).boxed());
app.at("/settings/update").get(|r| settings::update(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 // Subreddit services
app app
.at("/r/:sub") .at("/r/:sub")

View File

@ -1,3 +1,4 @@
use crate::{config, utils};
// CRATES // CRATES
use crate::utils::{ use crate::utils::{
catch_random, error, filter_posts, format_num, format_url, get_filters, nsfw_landing, param, redirect, rewrite_urls, setting, template, val, Post, Preferences, Subreddit, catch_random, error, filter_posts, format_num, format_url, get_filters, nsfw_landing, param, redirect, rewrite_urls, setting, template, val, Post, Preferences, Subreddit,
@ -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")] #[tokio::test(flavor = "multi_thread")]
async fn test_fetching_subreddit() { async fn test_fetching_subreddit() {
let subreddit = subreddit("rust", false).await; let subreddit = subreddit("rust", false).await;

View File

@ -2,6 +2,7 @@
use crate::client::json; use crate::client::json;
use crate::server::RequestExt; use crate::server::RequestExt;
use crate::utils::{error, filter_posts, format_url, get_filters, nsfw_landing, 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 crate::{config, utils};
use askama::Template; use askama::Template;
use hyper::{Body, Request, Response}; use hyper::{Body, Request, Response};
use time::{macros::format_description, OffsetDateTime}; use time::{macros::format_description, OffsetDateTime};
@ -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")] #[tokio::test(flavor = "multi_thread")]
async fn test_fetching_user() { async fn test_fetching_user() {
let user = user("spez").await; let user = user("spez").await;

View File

@ -1,5 +1,5 @@
#![allow(dead_code)] #![allow(dead_code)]
use crate::config::get_setting; use crate::config::{self, get_setting};
// //
// CRATES // CRATES
// //
@ -15,6 +15,7 @@ use serde_json::Value;
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::env; use std::env;
use std::str::FromStr; use std::str::FromStr;
use std::string::ToString;
use time::{macros::format_description, Duration, OffsetDateTime}; use time::{macros::format_description, Duration, OffsetDateTime};
use url::Url; use url::Url;
@ -327,6 +328,7 @@ pub struct Post {
pub gallery: Vec<GalleryMedia>, pub gallery: Vec<GalleryMedia>,
pub awards: Awards, pub awards: Awards,
pub nsfw: bool, pub nsfw: bool,
pub out_url: Option<String>,
pub ws_url: String, pub ws_url: String,
} }
@ -435,6 +437,7 @@ impl Post {
awards, awards,
nsfw: post["data"]["over_18"].as_bool().unwrap_or_default(), nsfw: post["data"]["over_18"].as_bool().unwrap_or_default(),
ws_url: val(post, "websocket_url"), 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())) Ok((posts, res["data"]["after"].as_str().unwrap_or_default().to_string()))
@ -770,6 +773,7 @@ pub async fn parse_post(post: &Value) -> Post {
awards, awards,
nsfw: post["data"]["over_18"].as_bool().unwrap_or_default(), nsfw: post["data"]["over_18"].as_bool().unwrap_or_default(),
ws_url: val(post, "websocket_url"), ws_url: val(post, "websocket_url"),
out_url: post["data"]["url_overridden_by_dest"].as_str().map(|a| a.to_string()),
} }
} }
@ -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. // Determines if a request shoud redirect to a nsfw landing gate.
pub fn should_be_nsfw_gated(req: &Request<Body>, req_url: &str) -> bool { pub fn should_be_nsfw_gated(req: &Request<Body>, req_url: &str) -> bool {
let sfw_instance = sfw_only(); let sfw_instance = sfw_only();
@ -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)] #[cfg(test)]
mod tests { mod tests {
use super::{format_num, format_url, rewrite_urls}; use super::{format_num, format_url, rewrite_urls};

File diff suppressed because it is too large Load Diff

View File

@ -27,7 +27,7 @@
{% call utils::sort(["/r/", sub.name.as_str()].concat(), ["hot", "new", "top", "rising", "controversial"], sort.0) %} {% call utils::sort(["/r/", sub.name.as_str()].concat(), ["hot", "new", "top", "rising", "controversial"], sort.0) %}
{% endif %} {% endif %}
</div> </div>
{% if sort.0 == "top" || sort.0 == "controversial" %}<select id="timeframe" name="t" title="Timeframe"> {% if sort.0 == "top" || sort.0 == "controversial" %}<select id="timeframe" name="t" title="Timeframe">
{% call utils::options(sort.1, ["hour", "day", "week", "month", "year", "all"], "day") %} {% call utils::options(sort.1, ["hour", "day", "week", "month", "year", "all"], "day") %}
</select> </select>
<button id="sort_submit" class="submit"> <button id="sort_submit" class="submit">
@ -130,7 +130,13 @@
</form> </form>
{% endif %} {% endif %}
</div> </div>
</div> {% if crate::utils::enable_rss() %}
<div id="sub_rss">
<a href="/r/{{ sub.name }}.rss" title="RSS feed for r/{{ sub.name }}">
<button class="subscribe">RSS feed</button >
</a>
</div>
{% endif %}
</div> </div>
</details> </details>
<details class="panel" id="sidebar"> <details class="panel" id="sidebar">

View File

@ -1,140 +1,178 @@
{% extends "base.html" %} {% extends "base.html" %} {% import "utils.html" as utils %} {% block search %}
{% import "utils.html" as utils %} {% call utils::search("".to_owned(), "") %} {% endblock %} {% block title %}{{
user.name.replace("u/", "") }} (u/{{ user.name }}) - Redlib{% endblock %} {%
block subscriptions %} {% call utils::sub_list("") %} {% endblock %} {% block
body %}
<main>
{% if !is_filtered %}
<div id="column_one">
<form id="sort">
<div id="listing_options">
{% call utils::sort(["/user/", user.name.as_str()].concat(),
["overview", "comments", "submitted"], listing) %}
</div>
<select id="sort_select" name="sort">
{% call utils::options(sort.0, ["hot", "new", "top",
"controversial"], "") %}</select
>{% if sort.0 == "top" || sort.0 == "controversial" %}<select
id="timeframe"
name="t"
>
{% call utils::options(sort.1, ["hour", "day", "week", "month",
"year", "all"], "all") %}</select
>{% endif %}<button id="sort_submit" class="submit">
<svg
width="15"
viewBox="0 0 110 100"
fill="none"
stroke-width="10"
stroke-linecap="round"
>
<path d="M20 50 H100" />
<path d="M75 15 L100 50 L75 85" />
&rarr;
</svg>
</button>
</form>
{% block search %} {% if all_posts_hidden_nsfw %}
{% call utils::search("".to_owned(), "") %} <center>
{% endblock %} All posts are hidden because they are NSFW. Enable "Show NSFW posts"
in settings to view.
{% block title %}{{ user.name.replace("u/", "") }} (u/{{ user.name }}) - Redlib{% endblock %} </center>
{% endif %} {% if no_posts %}
{% block subscriptions %} <center>No posts were found.</center>
{% call utils::sub_list("") %} {% endif %} {% if all_posts_filtered %}
{% endblock %} <center>(All content on this page has been filtered)</center>
{% else %}
{% block body %} <div id="posts">
<main> {% for post in posts %} {% if post.flags.nsfw && prefs.show_nsfw !=
{% if !is_filtered %} "on" %} {% else if !post.title.is_empty() %} {% call
<div id="column_one"> utils::post_in_list(post) %} {% else %}
<form id="sort"> <div class="comment user-comment">
<div id="listing_options"> <div class="comment_left">
{% call utils::sort(["/user/", user.name.as_str()].concat(), ["overview", "comments", "submitted"], listing) %} <p class="comment_score" title="{{ post.score.1 }}">
</div> {% if prefs.hide_score != "on" %} {{ post.score.0 }} {%
<select id="sort_select" name="sort"> else %} &#x2022; {% endif %}
{% call utils::options(sort.0, ["hot", "new", "top", "controversial"], "") %}
</select>{% if sort.0 == "top" || sort.0 == "controversial" %}<select id="timeframe" name="t">
{% call utils::options(sort.1, ["hour", "day", "week", "month", "year", "all"], "all") %}
</select>{% endif %}<button id="sort_submit" class="submit">
<svg width="15" viewBox="0 0 110 100" fill="none" stroke-width="10" stroke-linecap="round">
<path d="M20 50 H100" />
<path d="M75 15 L100 50 L75 85" />
&rarr;
</svg>
</button>
</form>
{% if all_posts_hidden_nsfw %}
<center>All posts are hidden because they are NSFW. Enable "Show NSFW posts" in settings to view.</center>
{% endif %}
{% if no_posts %}
<center>No posts were found.</center>
{% endif %}
{% if all_posts_filtered %}
<center>(All content on this page has been filtered)</center>
{% else %}
<div id="posts">
{% for post in posts %}
{% if post.flags.nsfw && prefs.show_nsfw != "on" %}
{% else if !post.title.is_empty() %}
{% call utils::post_in_list(post) %}
{% else %}
<div class="comment user-comment">
<div class="comment_left">
<p class="comment_score" title="{{ post.score.1 }}">
{% if prefs.hide_score != "on" %}
{{ post.score.0 }}
{% else %}
&#x2022;
{% endif %}
</p> </p>
<div class="line"></div> <div class="line"></div>
</div> </div>
<details class="comment_right" open> <details class="comment_right" open>
<summary class="comment_data"> <summary class="comment_data">
<a class="comment_link" href="{{ post.permalink }}" title="{{ post.link_title }}">{{ post.link_title }}</a> <a
<div class="user_comment_data_divider"> class="comment_link"
<span class="created-in">&nbsp;in&nbsp;</span> href="{{ post.permalink }}"
<a class="comment_subreddit" href="/r/{{ post.community }}">r/{{ post.community }}</a> title="{{ post.link_title }}"
<span class="dot">&bull;</span> >{{ post.link_title }}</a
<span class="created" title="{{ post.created }}">&nbsp;{{ post.rel_time }}</span> >
</div> <div class="user_comment_data_divider">
</summary> <span class="created-in">&nbsp;in&nbsp;</span>
<p class="comment_body">{{ post.body|safe }}</p> <a
</details> class="comment_subreddit"
</div> href="/r/{{ post.community }}"
{% endif %} >r/{{ post.community }}</a
{% endfor %} >
{% if prefs.use_hls == "on" %} <span class="dot">&bull;</span>
<script src="/hls.min.js"></script> <span class="created" title="{{ post.created }}"
<script src="/playHLSVideo.js"></script> >&nbsp;{{ post.rel_time }}</span
{% endif %} >
</div> </div>
{% endif %} </summary>
<p class="comment_body">{{ post.body|safe }}</p>
</details>
</div>
{% endif %} {% endfor %} {% if prefs.use_hls == "on" %}
<script src="/hls.min.js"></script>
<script src="/playHLSVideo.js"></script>
{% endif %}
</div>
{% endif %}
<footer> <footer>
{% if ends.0 != "" %} {% if ends.0 != "" %}
<a href="?sort={{ sort.0 }}&t={{ sort.1 }}&before={{ ends.0 }}" accesskey="P">PREV</a> <a
{% endif %} href="?sort={{ sort.0 }}&t={{ sort.1 }}&before={{ ends.0 }}"
accesskey="P"
{% if ends.1 != "" %} >PREV</a
<a href="?sort={{ sort.0 }}&t={{ sort.1 }}&after={{ ends.1 }}" accesskey="N">NEXT</a> >
{% endif %} {% endif %} {% if ends.1 != "" %}
</footer> <a
</div> href="?sort={{ sort.0 }}&t={{ sort.1 }}&after={{ ends.1 }}"
{% endif %} accesskey="N"
<aside> >NEXT</a
{% if is_filtered %} >
<center>(Content from u/{{ user.name }} has been filtered)</center> {% endif %}
{% endif %} </footer>
<div class="panel" id="user"> </div>
<img loading="lazy" id="user_icon" src="{{ user.icon }}" alt="User icon"> {% endif %}
<h1 id="user_title">{{ user.title }}</h1> <aside>
<p id="user_name">u/{{ user.name }}</p> {% if is_filtered %}
<div id="user_description">{{ user.description }}</div> <center>(Content from u/{{ user.name }} has been filtered)</center>
<div id="user_details"> {% endif %}
<label>Karma</label> <div class="panel" id="user">
<label>Created</label> <img
<div>{{ user.karma }}</div> loading="lazy"
<div>{{ user.created }}</div> id="user_icon"
</div> src="{{ user.icon }}"
<div id="user_actions"> alt="User icon"
{% let name = ["u_", user.name.as_str()].join("") %} />
<div id="user_subscription"> <h1 id="user_title">{{ user.title }}</h1>
{% if prefs.subscriptions.contains(name) %} <p id="user_name">u/{{ user.name }}</p>
<form action="/r/{{ name }}/unsubscribe?redirect={{ redirect_url }}" method="POST"> <div id="user_description">{{ user.description }}</div>
<button class="unsubscribe">Unfollow</button> <div id="user_details">
</form> <label>Karma</label>
{% else %} <label>Created</label>
<form action="/r/{{ name }}/subscribe?redirect={{ redirect_url }}" method="POST"> <div>{{ user.karma }}</div>
<button class="subscribe">Follow</button> <div>{{ user.created }}</div>
</form> </div>
{% endif %} <div id="user_actions">
</div> {% let name = ["u_", user.name.as_str()].join("") %}
<div id="user_filter"> <div id="user_subscription">
{% if prefs.filters.contains(name) %} {% if prefs.subscriptions.contains(name) %}
<form action="/r/{{ name }}/unfilter?redirect={{ redirect_url }}" method="POST"> <form
<button class="unfilter">Unfilter</button> action="/r/{{ name }}/unsubscribe?redirect={{ redirect_url }}"
</form> method="POST"
{% else %} >
<form action="/r/{{ name }}/filter?redirect={{ redirect_url }}" method="POST"> <button class="unsubscribe">Unfollow</button>
<button class="filter">Filter</button> </form>
</form> {% else %}
{% endif %} <form
</div> action="/r/{{ name }}/subscribe?redirect={{ redirect_url }}"
</div> method="POST"
</div> >
</aside> <button class="subscribe">Follow</button>
</main> </form>
{% endif %}
</div>
<div id="user_filter">
{% if prefs.filters.contains(name) %}
<form
action="/r/{{ name }}/unfilter?redirect={{ redirect_url }}"
method="POST"
>
<button class="unfilter">Unfilter</button>
</form>
{% else %}
<form
action="/r/{{ name }}/filter?redirect={{ redirect_url }}"
method="POST"
>
<button class="filter">Filter</button>
</form>
{% endif %}
</div>
{% if crate::utils::enable_rss() %}
<div id="user_rss">
<a
href="/u/{{ user.name }}.rss"
title="RSS feed for u/{{ user.name }}"
>
<button class="subscribe">RSS feed</button>
</a>
</div>
{% endif %}
</div>
</div>
</aside>
</main>
{% endblock %} {% endblock %}