Compare commits

...

7 Commits

Author SHA1 Message Date
85deb4947d Support HLS playback in search and user feeds 2021-06-11 17:38:43 -07:00
d2002c9027 Disable dysfunctional moderator list feature 2021-06-11 11:03:36 -07:00
f84f4c0326 Add Trevor instance 2021-05-31 04:55:39 +00:00
ca3f6c0579 Fix #228 2021-05-28 12:01:20 -07:00
decc9e5139 Include SystemD configuration (#227) 2021-05-28 04:33:14 +00:00
d27bd782ce Specify fallback fonts 2021-05-26 20:30:08 -07:00
4defb58f2a Optimizations and commenting 2021-05-20 12:24:06 -07:00
18 changed files with 381 additions and 405 deletions

76
Cargo.lock generated
View File

@ -127,9 +127,9 @@ dependencies = [
[[package]]
name = "bumpalo"
version = "3.6.1"
version = "3.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63396b8a4b9de3f4fdfb320ab6080762242f66a8ef174c49d8e19b674db4cdbe"
checksum = "9c59e7af012c713f529e7a3ee57ce9b31ddd858d4b512923602f74608b009631"
[[package]]
name = "bytes"
@ -173,9 +173,9 @@ checksum = "3a4f925191b4367301851c6d99b09890311d74b0d43f274c0b34c86d308a3663"
[[package]]
name = "cc"
version = "1.0.67"
version = "1.0.68"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3c69b077ad434294d3ce9f1f6143a2a4b89a8a2d54ef813d85003a4fd1137fd"
checksum = "4a72c244c1ff497a746a7e1fb3d14bd08420ecda70c8f25c7112f2781652d787"
[[package]]
name = "cfg-if"
@ -363,9 +363,9 @@ checksum = "acc499defb3b348f8d8f3f66415835a9131856ff7714bf10dadfc4ec4bdb29a1"
[[package]]
name = "futures-lite"
version = "1.11.3"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4481d0cd0de1d204a4fa55e7d45f07b1d958abcb06714b3446438e2eff695fb"
checksum = "7694489acd39452c77daa48516b894c153f192c3578d5a839b62c58099fcbf48"
dependencies = [
"fastrand",
"futures-core",
@ -486,15 +486,15 @@ checksum = "f3a87b616e37e93c22fb19bcd386f02f3af5ea98a25670ad0fce773de23c5e68"
[[package]]
name = "httpdate"
version = "1.0.0"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05842d0d43232b23ccb7060ecb0f0626922c21f30012e97b767b30afd4a5d4b9"
checksum = "6456b8a6c8f33fee7d958fcd1b60d55b11940a79e63ae87013e6d22e26034440"
[[package]]
name = "hyper"
version = "0.14.7"
version = "0.14.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e5f105c494081baa3bf9e200b279e27ec1623895cd504c7dbef8d0b080fcf54"
checksum = "07d6baa1b441335f3ce5098ac421fb6547c46dda735ca1bc6d0153c838f9dd83"
dependencies = [
"bytes",
"futures-channel",
@ -506,7 +506,7 @@ dependencies = [
"httparse",
"httpdate",
"itoa",
"pin-project",
"pin-project-lite",
"socket2",
"tokio",
"tower-service",
@ -603,13 +603,13 @@ dependencies = [
[[package]]
name = "libc"
version = "0.2.94"
version = "0.2.96"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18794a8ad5b29321f790b55d93dfba91e125cb1a9edbd4f8e3150acc771c1a5e"
checksum = "5600b4e6efc5421841a2138a6b082e07fe12f9aaa12783d50e5d13325b26b4fc"
[[package]]
name = "libreddit"
version = "0.14.3"
version = "0.14.8"
dependencies = [
"askama",
"async-recursion",
@ -761,26 +761,6 @@ version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
[[package]]
name = "pin-project"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7509cc106041c40a4518d2af7a61530e1eed0e6285296a3d8c5472806ccc4a4"
dependencies = [
"pin-project-internal",
]
[[package]]
name = "pin-project-internal"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c950132583b500556b1efd71d45b319029f2b71518d979fcc208e16b42426f"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "pin-project-lite"
version = "0.2.6"
@ -807,9 +787,9 @@ checksum = "bc881b2c22681370c6a780e47af9840ef841837bc98118431d4e1868bd0c1086"
[[package]]
name = "proc-macro2"
version = "1.0.26"
version = "1.0.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a152013215dca273577e18d2bf00fa862b89b24169fb78c4c95aeb07992c9cec"
checksum = "f0d8caf72986c1a598726adc988bb5984792ef84f5ee5aa50209145ee8077038"
dependencies = [
"unicode-xid",
]
@ -944,9 +924,9 @@ dependencies = [
[[package]]
name = "security-framework"
version = "2.2.0"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3670b1d2fdf6084d192bc71ead7aabe6c06aa2ea3fbd9cc3ac111fa5c2b1bd84"
checksum = "23a2ac85147a3a11d77ecf1bc7166ec0b92febfa4461c37944e180f319ece467"
dependencies = [
"bitflags",
"core-foundation",
@ -957,9 +937,9 @@ dependencies = [
[[package]]
name = "security-framework-sys"
version = "2.2.0"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3676258fd3cfe2c9a0ec99ce3038798d847ce3e4bb17746373eb9f0f1ac16339"
checksum = "7e4effb91b4b8b6fb7732e670b6cee160278ff8e6bf485c7805d9e319d76e284"
dependencies = [
"core-foundation-sys",
"libc",
@ -1019,9 +999,9 @@ checksum = "2579985fda508104f7587689507983eadd6a6e84dd35d6d115361f530916fa0d"
[[package]]
name = "signal-hook-registry"
version = "1.3.0"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16f1d0fef1604ba8f7a073c7e701f213e056707210e9020af4528e0101ce11a6"
checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0"
dependencies = [
"libc",
]
@ -1126,9 +1106,9 @@ checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c"
[[package]]
name = "syn"
version = "1.0.72"
version = "1.0.73"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1e8cdbefb79a9a5a65e0db8b47b723ee907b7c7f8496c76a1770b5c310bab82"
checksum = "f71489ff30030d2ae598524f61326b902466f72a0fb1a8564c001cc63425bcc7"
dependencies = [
"proc-macro2",
"quote",
@ -1205,9 +1185,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
[[package]]
name = "tokio"
version = "1.6.0"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd3076b5c8cc18138b8f8814895c11eb4de37114a5d127bafdc5e55798ceef37"
checksum = "0a38d31d7831c6ed7aad00aa4c12d9375fd225a6dd77da1d25b707346319a975"
dependencies = [
"autocfg",
"bytes",
@ -1302,9 +1282,9 @@ dependencies = [
[[package]]
name = "unicode-normalization"
version = "0.1.17"
version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07fbfce1c8a97d547e8b5334978438d9d6ec8c20e38f56d4a4374d181493eaef"
checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9"
dependencies = [
"tinyvec",
]

View File

@ -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.14.3"
version = "0.14.8"
authors = ["spikecodes <19519553+spikecodes@users.noreply.github.com>"]
edition = "2018"
@ -15,11 +15,11 @@ clap = { version = "2.33.3", default-features = false }
regex = "1.5.4"
serde = { version = "1.0.126", features = ["derive"] }
cookie = "0.15.0"
futures-lite = "1.11.3"
hyper = { version = "0.14.7", features = ["full"] }
futures-lite = "1.12.0"
hyper = { version = "0.14.9", features = ["full"] }
hyper-rustls = "0.22.1"
route-recognizer = "0.3.0"
serde_json = "1.0.64"
tokio = { version = "1.6.0", features = ["full"] }
tokio = { version = "1.6.1", features = ["full"] }
time = "0.2.26"
url = "2.2.2"

View File

@ -40,10 +40,11 @@ Feel free to [open an issue](https://github.com/spikecodes/libreddit/issues/new)
| [libreddit.database.red](https://libreddit.database.red) | 🇺🇸 US | ✅ |
| [libreddit.exonip.de](https://libreddit.exonip.de) | 🇩🇪 DE | |
| [libreddit.domain.glass](https://libreddit.domain.glass) | 🇺🇸 US | ✅ |
| [libreddit.trevorthalacker.com](https://libreddit.trevorthalacker.com) | 🇺🇸 US | ✅ |
| [spjmllawtheisznfs7uryhxumin26ssv2draj7oope3ok3wuhy43eoyd.onion](http://spjmllawtheisznfs7uryhxumin26ssv2draj7oope3ok3wuhy43eoyd.onion) | 🇮🇳 IN | |
| [fwhhsbrbltmrct5hshrnqlqygqvcgmnek3cnka55zj4y7nuus5muwyyd.onion](http://fwhhsbrbltmrct5hshrnqlqygqvcgmnek3cnka55zj4y7nuus5muwyyd.onion) | 🇩🇪 DE | |
| [dflv6yjt7il3n3tggf4qhcmkzbti2ppytqx3o7pjrzwgntutpewscyid.onion](http://dflv6yjt7il3n3tggf4qhcmkzbti2ppytqx3o7pjrzwgntutpewscyid.onion/) | 🇺🇸 US | |
| [kphht2jcflojtqte4b4kyx7p2ahagv4debjj32nre67dxz7y57seqwyd.onion](http://kphht2jcflojtqte4b4kyx7p2ahagv4debjj32nre67dxz7y57seqwyd.onion/) | 🇳🇱 NL | |
| [dflv6yjt7il3n3tggf4qhcmkzbti2ppytqx3o7pjrzwgntutpewscyid.onion](http://dflv6yjt7il3n3tggf4qhcmkzbti2ppytqx3o7pjrzwgntutpewscyid.onion) | 🇺🇸 US | |
| [kphht2jcflojtqte4b4kyx7p2ahagv4debjj32nre67dxz7y57seqwyd.onion](http://kphht2jcflojtqte4b4kyx7p2ahagv4debjj32nre67dxz7y57seqwyd.onion) | 🇳🇱 NL | |
A checkmark in the "Cloudflare" category here refers to the use of the reverse proxy, [Cloudflare](https://cloudflare). The checkmark will not be listed for a site which uses Cloudflare DNS but rather the proxying service which grants Cloudflare the ability to monitor traffic to the website.
@ -198,17 +199,17 @@ libreddit
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.
| Name | Possible values | Default value |
|-----------------------|----------------------------------------------------------------------------------------|---------------|
| theme | ["system", "light", "dark", "black", "dracula", "nord", "laserwave", "violet", "gold"] | system |
| front_page | ["default", "popular", "all"] | default |
| layout | ["card", "clean", "compact"] | card |
| wide | ["on", "off"] | off |
| comment_sort | ["hot", "new", "top", "rising", "controversial"] | hot |
| post_sort | ["confidence", "top", "new", "controversial", "old"] | confidence |
| show_nsfw | ["on", "off"] | off |
| use_hls | ["on", "off"] | off |
| hide_hls_notification | ["on", "off"] | off |
| Name | Possible values | Default value |
|-------------------------|------------------------------------------------------------------------------------------|---------------|
| `THEME` | `["system", "light", "dark", "black", "dracula", "nord", "laserwave", "violet", "gold"]` | `system` |
| `FRONT_PAGE` | `["default", "popular", "all"]` | `default` |
| `LAYOUT` | `["card", "clean", "compact"]` | `card` |
| `WIDE` | `["on", "off"]` | `off` |
| `COMMENT_SORT` | `["hot", "new", "top", "rising", "controversial"]` | `hot` |
| `POST_SORT` | `["confidence", "top", "new", "controversial", "old"]` | `confidence` |
| `SHOW_NSFW` | `["on", "off"]` | `off` |
| `USE_HLS` | `["on", "off"]` | `off` |
| `HIDE_HLS_NOTIFICATION` | `["on", "off"]` | `off` |
### Examples
@ -228,6 +229,25 @@ proxy_http_version 1.1;
```
to your NGINX configuration file above your `proxy_pass` line.
## SystemD
You can use the SystemD service available in `contrib/libreddit.service`
(install it on `/etc/systemd/system/libreddit.service`).
That service can be optionally configured in terms of environment variables by
creating a file in `/etc/libreddit.conf`. Use the `contrib/libreddit.conf` as a
template. You can also add the `LIBREDDIT_DEFAULT__{X}` settings explained
above.
When "Proxying using NGINX" where the proxy is on the same machine, you should
guarantee nginx waits for this service to start. Edit
`/etc/systemd/system/libreddit.service.d/reverse-proxy.conf`:
```conf
[Unit]
Before=nginx.service
```
## Building
```

2
contrib/libreddit.conf Normal file
View File

@ -0,0 +1,2 @@
ADDRESS=localhost
PORT=12345

15
contrib/libreddit.service Normal file
View File

@ -0,0 +1,15 @@
[Unit]
Description=libreddit daemon
After=network.service
[Service]
DynamicUser=yes
# Default Values
Environment=ADDRESS=0.0.0.0
Environment=PORT=8080
# Optional Override
EnvironmentFile=-/etc/libreddit.conf
ExecStart=/usr/bin/libreddit -a ${ADDRESS} -p ${PORT}
[Install]
WantedBy=default.target

View File

@ -9,7 +9,9 @@ use crate::server::RequestExt;
pub async fn proxy(req: Request<Body>, format: &str) -> Result<Response<Body>, String> {
let mut url = format!("{}?{}", format, req.uri().query().unwrap_or_default());
// For each parameter in request
for (name, value) in req.params().iter() {
// Fill the parameter value in the url
url = url.replace(&format!("{{{}}}", name), value);
}
@ -29,14 +31,13 @@ async fn stream(url: &str, req: &Request<Body>) -> Result<Response<Body>, String
let mut builder = Request::get(url);
// Copy useful headers from original request
let headers = req.headers();
for &key in &["Range", "If-Modified-Since", "Cache-Control"] {
if let Some(value) = headers.get(key) {
if let Some(value) = req.headers().get(key) {
builder = builder.header(key, value);
}
}
let stream_request = builder.body(Body::default()).expect("stream");
let stream_request = builder.body(Body::empty()).map_err(|_| "Couldn't build empty body in stream".to_string())?;
client
.request(stream_request)
@ -64,9 +65,10 @@ fn request(url: String, quarantine: bool) -> Boxed<Result<Response<Body>, String
// Prepare the HTTPS connector.
let https = hyper_rustls::HttpsConnector::with_native_roots();
// Build the hyper client from the HTTPS connector.
// Construct the hyper client from the HTTPS connector.
let client: client::Client<_, hyper::Body> = client::Client::builder().build(https);
// Build request
let builder = Request::builder()
.method("GET")
.uri(&url)

View File

@ -1,14 +1,7 @@
// Global specifiers
#![forbid(unsafe_code)]
#![warn(clippy::pedantic, clippy::all)]
#![allow(
clippy::needless_pass_by_value,
clippy::match_wildcard_for_single_variants,
clippy::cast_possible_truncation,
clippy::similar_names,
clippy::cast_possible_wrap,
clippy::find_map
)]
#![allow(clippy::needless_pass_by_value, clippy::cast_possible_truncation, clippy::cast_possible_wrap, clippy::manual_find_map)]
// Reference local files
mod post;
@ -97,6 +90,13 @@ async fn main() {
let matches = cli::new("Libreddit")
.version(env!("CARGO_PKG_VERSION"))
.about("Private front-end for Reddit written in Rust ")
.arg(
Arg::with_name("redirect-https")
.short("r")
.long("redirect-https")
.help("Redirect all HTTP requests to HTTPS (no longer functional)")
.takes_value(false),
)
.arg(
Arg::with_name("address")
.short("a")
@ -115,13 +115,6 @@ async fn main() {
.default_value("8080")
.takes_value(true),
)
.arg(
Arg::with_name("redirect-https")
.short("r")
.long("redirect-https")
.help("Redirect all HTTP requests to HTTPS (no longer functional)")
.takes_value(false),
)
.arg(
Arg::with_name("hsts")
.short("H")
@ -137,7 +130,7 @@ async fn main() {
let port = matches.value_of("port").unwrap_or("8080");
let hsts = matches.value_of("hsts");
let listener = format!("{}:{}", address, port);
let listener = [address, ":", port].concat();
println!("Starting Libreddit...");

View File

@ -7,8 +7,6 @@ use crate::utils::{error, format_num, format_url, param, rewrite_urls, setting,
use hyper::{Body, Request, Response};
use async_recursion::async_recursion;
use askama::Template;
// STRUCTS
@ -52,10 +50,10 @@ pub async fn item(req: Request<Body>) -> Result<Response<Body>, String> {
// Send a request to the url, receive JSON in response
match json(path, quarantined).await {
// Otherwise, grab the JSON output from the request
Ok(res) => {
Ok(response) => {
// Parse the JSON into Post and Comment structs
let post = parse_post(&res[0]).await;
let comments = parse_comments(&res[1], &post.permalink, &post.author.name, highlighted_comment).await;
let post = parse_post(&response[0]).await;
let comments = parse_comments(&response[1], &post.permalink, &post.author.name, highlighted_comment);
// Use the Post and Comment structs to generate a website to show users
template(PostTemplate {
@ -151,79 +149,71 @@ async fn parse_post(json: &serde_json::Value) -> Post {
}
// COMMENTS
#[async_recursion]
async fn parse_comments(json: &serde_json::Value, post_link: &str, post_author: &str, highlighted_comment: &str) -> Vec<Comment> {
// Separate the comment JSON into a Vector of comments
let comment_data = match json["data"]["children"].as_array() {
Some(f) => f.to_owned(),
None => Vec::new(),
};
let mut comments: Vec<Comment> = Vec::new();
fn parse_comments(json: &serde_json::Value, post_link: &str, post_author: &str, highlighted_comment: &str) -> Vec<Comment> {
// Parse the comment JSON into a Vector of Comments
let comments = json["data"]["children"].as_array().map_or(Vec::new(), std::borrow::ToOwned::to_owned);
// For each comment, retrieve the values to build a Comment object
for comment in comment_data {
let kind = comment["kind"].as_str().unwrap_or_default().to_string();
let data = &comment["data"];
let unix_time = data["created_utc"].as_f64().unwrap_or_default();
let (rel_time, created) = time(unix_time);
let edited = match data["edited"].as_f64() {
Some(stamp) => time(stamp),
None => (String::new(), String::new()),
};
let score = data["score"].as_i64().unwrap_or(0);
let body = rewrite_urls(&val(&comment, "body_html"));
// If this comment contains replies, handle those too
let replies: Vec<Comment> = if data["replies"].is_object() {
parse_comments(&data["replies"], post_link, post_author, highlighted_comment).await
} else {
Vec::new()
};
let parent_kind_and_id = val(&comment, "parent_id");
let parent_info = parent_kind_and_id.split('_').collect::<Vec<&str>>();
let id = val(&comment, "id");
let highlighted = id == highlighted_comment;
comments.push(Comment {
id,
kind,
parent_id: parent_info[1].to_string(),
parent_kind: parent_info[0].to_string(),
post_link: post_link.to_string(),
post_author: post_author.to_string(),
body,
author: Author {
name: val(&comment, "author"),
flair: Flair {
flair_parts: FlairPart::parse(
data["author_flair_type"].as_str().unwrap_or_default(),
data["author_flair_richtext"].as_array(),
data["author_flair_text"].as_str(),
),
text: esc!(&comment, "link_flair_text"),
background_color: val(&comment, "author_flair_background_color"),
foreground_color: val(&comment, "author_flair_text_color"),
},
distinguished: val(&comment, "distinguished"),
},
score: if data["score_hidden"].as_bool().unwrap_or_default() {
("\u{2022}".to_string(), "Hidden".to_string())
} else {
format_num(score)
},
rel_time,
created,
edited,
replies,
highlighted,
});
}
comments
.into_iter()
.map(|comment| {
let kind = comment["kind"].as_str().unwrap_or_default().to_string();
let data = &comment["data"];
let unix_time = data["created_utc"].as_f64().unwrap_or_default();
let (rel_time, created) = time(unix_time);
let edited = data["edited"].as_f64().map_or((String::new(), String::new()), time);
let score = data["score"].as_i64().unwrap_or(0);
let body = rewrite_urls(&val(&comment, "body_html"));
// If this comment contains replies, handle those too
let replies: Vec<Comment> = if data["replies"].is_object() {
parse_comments(&data["replies"], post_link, post_author, highlighted_comment)
} else {
Vec::new()
};
let parent_kind_and_id = val(&comment, "parent_id");
let parent_info = parent_kind_and_id.split('_').collect::<Vec<&str>>();
let id = val(&comment, "id");
let highlighted = id == highlighted_comment;
Comment {
id,
kind,
parent_id: parent_info[1].to_string(),
parent_kind: parent_info[0].to_string(),
post_link: post_link.to_string(),
post_author: post_author.to_string(),
body,
author: Author {
name: val(&comment, "author"),
flair: Flair {
flair_parts: FlairPart::parse(
data["author_flair_type"].as_str().unwrap_or_default(),
data["author_flair_richtext"].as_array(),
data["author_flair_text"].as_str(),
),
text: esc!(&comment, "link_flair_text"),
background_color: val(&comment, "author_flair_background_color"),
foreground_color: val(&comment, "author_flair_text_color"),
},
distinguished: val(&comment, "distinguished"),
},
score: if data["score_hidden"].as_bool().unwrap_or_default() {
("\u{2022}".to_string(), "Hidden".to_string())
} else {
format_num(score)
},
rel_time,
created,
edited,
replies,
highlighted,
}
})
.collect()
}

View File

@ -52,10 +52,8 @@ pub async fn find(req: Request<Body>) -> Result<Response<Body>, String> {
let sort = param(&path, "sort").unwrap_or_else(|| "relevance".to_string());
let subreddits = match param(&path, "restrict_sr") {
None => search_subreddits(&query).await,
Some(_) => Vec::new(),
};
// If search is not restricted to this subreddit, show other subreddits in search results
let subreddits = param(&path, "restrict_sr").map_or(search_subreddits(&query).await, |_| Vec::new());
let url = String::from(req.uri().path_and_query().map_or("", |val| val.as_str()));
@ -90,35 +88,25 @@ async fn search_subreddits(q: &str) -> Vec<Subreddit> {
let subreddit_search_path = format!("/subreddits/search.json?q={}&limit=3", q.replace(' ', "+"));
// Send a request to the url
match json(subreddit_search_path, false).await {
// If success, receive JSON in response
Ok(response) => {
match response["data"]["children"].as_array() {
// For each subreddit from subreddit list
Some(list) => list
.iter()
.map(|subreddit| {
// Fetch subreddit icon either from the community_icon or icon_img value
let community_icon: &str = subreddit["data"]["community_icon"].as_str().map_or("", |s| s.split('?').collect::<Vec<&str>>()[0]);
let icon = if community_icon.is_empty() {
val(&subreddit, "icon_img")
} else {
community_icon.to_string()
};
json(subreddit_search_path, false).await.unwrap_or_default()["data"]["children"]
.as_array()
.map(ToOwned::to_owned)
.unwrap_or_default()
.iter()
.map(|subreddit| {
// For each subreddit from subreddit list
// Fetch subreddit icon either from the community_icon or icon_img value
let icon = subreddit["data"]["community_icon"]
.as_str()
.map_or_else(|| val(&subreddit, "icon_img"), ToString::to_string);
Subreddit {
name: val(subreddit, "display_name_prefixed"),
url: val(subreddit, "url"),
icon: format_url(&icon),
description: val(subreddit, "public_description"),
subscribers: format_num(subreddit["data"]["subscribers"].as_f64().unwrap_or_default() as i64),
}
})
.collect::<Vec<Subreddit>>(),
_ => Vec::new(),
Subreddit {
name: val(subreddit, "display_name_prefixed"),
url: val(subreddit, "url"),
icon: format_url(&icon),
description: val(subreddit, "public_description"),
subscribers: format_num(subreddit["data"]["subscribers"].as_f64().unwrap_or_default() as i64),
}
}
// If the Reddit API returns an error, exit this function
_ => Vec::new(),
}
})
.collect::<Vec<Subreddit>>()
}

View File

@ -53,7 +53,7 @@ pub trait ResponseExt {
impl RequestExt for Request<Body> {
fn params(&self) -> Params {
self.extensions().get::<Params>().unwrap_or(&Params::new()).to_owned()
self.extensions().get::<Params>().unwrap_or(&Params::new()).clone()
// self.extensions()
// .get::<RequestMeta>()
// .and_then(|meta| meta.route_params())
@ -69,29 +69,31 @@ impl RequestExt for Request<Body> {
}
fn cookies(&self) -> Vec<Cookie> {
let mut cookies = Vec::new();
if let Some(header) = self.headers().get("Cookie") {
for cookie in header.to_str().unwrap_or_default().split("; ") {
cookies.push(Cookie::parse(cookie).unwrap_or_else(|_| Cookie::named("")));
}
}
cookies
self.headers().get("Cookie").map_or(Vec::new(), |header| {
header
.to_str()
.unwrap_or_default()
.split("; ")
.map(|cookie| Cookie::parse(cookie).unwrap_or_else(|_| Cookie::named("")))
.collect()
})
}
fn cookie(&self, name: &str) -> Option<Cookie> {
self.cookies().iter().find(|c| c.name() == name).map(std::borrow::ToOwned::to_owned)
self.cookies().into_iter().find(|c| c.name() == name)
}
}
impl ResponseExt for Response<Body> {
fn cookies(&self) -> Vec<Cookie> {
let mut cookies = Vec::new();
for header in self.headers().get_all("Cookie") {
if let Ok(cookie) = Cookie::parse(header.to_str().unwrap_or_default()) {
cookies.push(cookie);
}
}
cookies
self.headers().get("Cookie").map_or(Vec::new(), |header| {
header
.to_str()
.unwrap_or_default()
.split("; ")
.map(|cookie| Cookie::parse(cookie).unwrap_or_else(|_| Cookie::named("")))
.collect()
})
}
fn insert_cookie(&mut self, cookie: Cookie) {
@ -144,6 +146,7 @@ impl Server {
pub fn listen(self, addr: String) -> Boxed<Result<(), hyper::Error>> {
let make_svc = make_service_fn(move |_conn| {
// For correct borrowing, these values need to be borrowed
let router = self.router.clone();
let default_headers = self.default_headers.clone();
@ -159,7 +162,7 @@ impl Server {
let mut path = req.uri().path().replace("//", "/");
// Remove trailing slashes
if path.ends_with('/') && path != "/" {
if path != "/" && path.ends_with('/') {
path.pop();
}
@ -168,7 +171,7 @@ impl Server {
// If a route was configured for this path
Ok(found) => {
let mut parammed = req;
parammed.set_params(found.params().to_owned());
parammed.set_params(found.params().clone());
// Run the route's function
let func = (found.handler().to_owned().to_owned())(parammed);
@ -198,17 +201,15 @@ impl Server {
}
});
// Build SocketAddr from provided address
let address = &addr.parse().unwrap_or_else(|_| panic!("Cannot parse {} as address (example format: 0.0.0.0:8080)", addr));
let server = HyperServer::bind(address).serve(make_svc);
// Bind server to address specified above. Gracefully shut down if CTRL+C is pressed
let server = HyperServer::bind(address).serve(make_svc).with_graceful_shutdown(async {
// Wait for the CTRL+C signal
tokio::signal::ctrl_c().await.expect("Failed to install CTRL+C signal handler")
});
let graceful = server.with_graceful_shutdown(shutdown_signal());
graceful.boxed()
server.boxed()
}
}
async fn shutdown_signal() {
// Wait for the CTRL+C signal
tokio::signal::ctrl_c().await.expect("Failed to install CTRL+C signal handler");
}

View File

@ -43,12 +43,12 @@ pub async fn set(req: Request<Body>) -> Result<Response<Body>, String> {
let (parts, mut body) = req.into_parts();
// Grab existing cookies
let mut cookies = Vec::new();
for header in parts.headers.get_all("Cookie") {
if let Ok(cookie) = Cookie::parse(header.to_str().unwrap_or_default()) {
cookies.push(cookie);
}
}
let _cookies: Vec<Cookie> = parts
.headers
.get_all("Cookie")
.iter()
.filter_map(|header| Cookie::parse(header.to_str().unwrap_or_default()).ok())
.collect();
// Aggregate the body...
// let whole_body = hyper::body::aggregate(req).await.map_err(|e| e.to_string())?;
@ -62,22 +62,22 @@ pub async fn set(req: Request<Body>) -> Result<Response<Body>, String> {
let form = url::form_urlencoded::parse(&body_bytes).collect::<HashMap<_, _>>();
let mut res = redirect("/settings".to_string());
let mut response = redirect("/settings".to_string());
for &name in &PREFS {
match form.get(name) {
Some(value) => res.insert_cookie(
Cookie::build(name.to_owned(), value.to_owned())
Some(value) => response.insert_cookie(
Cookie::build(name.to_owned(), value.clone())
.path("/")
.http_only(true)
.expires(OffsetDateTime::now_utc() + Duration::weeks(52))
.finish(),
),
None => res.remove_cookie(name.to_string()),
None => response.remove_cookie(name.to_string()),
};
}
Ok(res)
Ok(response)
}
fn set_cookies_method(req: Request<Body>, remove_cookies: bool) -> Response<Body> {
@ -85,12 +85,12 @@ fn set_cookies_method(req: Request<Body>, remove_cookies: bool) -> Response<Body
let (parts, _) = req.into_parts();
// Grab existing cookies
let mut cookies = Vec::new();
for header in parts.headers.get_all("Cookie") {
if let Ok(cookie) = Cookie::parse(header.to_str().unwrap_or_default()) {
cookies.push(cookie);
}
}
let _cookies: Vec<Cookie> = parts
.headers
.get_all("Cookie")
.iter()
.filter_map(|header| Cookie::parse(header.to_str().unwrap_or_default()).ok())
.collect();
let query = parts.uri.query().unwrap_or_default().as_bytes();
@ -101,12 +101,12 @@ fn set_cookies_method(req: Request<Body>, remove_cookies: bool) -> Response<Body
None => "/".to_string(),
};
let mut res = redirect(path);
let mut response = redirect(path);
for name in [PREFS.to_vec(), vec!["subscriptions"]].concat() {
match form.get(name) {
Some(value) => res.insert_cookie(
Cookie::build(name.to_owned(), value.to_owned())
Some(value) => response.insert_cookie(
Cookie::build(name.to_owned(), value.clone())
.path("/")
.http_only(true)
.expires(OffsetDateTime::now_utc() + Duration::weeks(52))
@ -114,13 +114,13 @@ fn set_cookies_method(req: Request<Body>, remove_cookies: bool) -> Response<Body
),
None => {
if remove_cookies {
res.remove_cookie(name.to_string())
response.remove_cookie(name.to_string())
}
}
};
}
res
response
}
// Set cookies using response "Set-Cookie" header

View File

@ -51,10 +51,10 @@ pub async fn community(req: Request<Body>) -> Result<Response<Body>, String> {
if subscribed.is_empty() {
"popular".to_string()
} else {
subscribed.to_owned()
subscribed.clone()
}
} else {
front_page.to_owned()
front_page.clone()
});
let quarantined = can_access_quarantine(&req, &sub) || root;
@ -133,15 +133,15 @@ pub fn quarantine(req: Request<Body>, sub: String) -> Result<Response<Body>, Str
pub async fn add_quarantine_exception(req: Request<Body>) -> Result<Response<Body>, String> {
let subreddit = req.param("sub").ok_or("Invalid URL")?;
let redir = param(&format!("?{}", req.uri().query().unwrap_or_default()), "redir").ok_or("Invalid URL")?;
let mut res = redirect(redir);
res.insert_cookie(
let mut response = redirect(redir);
response.insert_cookie(
Cookie::build(&format!("allow_quaran_{}", subreddit.to_lowercase()), "true")
.path("/")
.http_only(true)
.expires(cookie::Expiration::Session)
.finish(),
);
Ok(res)
Ok(response)
}
pub fn can_access_quarantine(req: &Request<Body>, sub: &str) -> bool {
@ -211,13 +211,13 @@ pub async fn subscriptions(req: Request<Body>) -> Result<Response<Body>, String>
format!("/r/{}", sub)
};
let mut res = redirect(path);
let mut response = redirect(path);
// Delete cookie if empty, else set
if sub_list.is_empty() {
res.remove_cookie("subscriptions".to_string());
response.remove_cookie("subscriptions".to_string());
} else {
res.insert_cookie(
response.insert_cookie(
Cookie::build("subscriptions", sub_list.join("+"))
.path("/")
.http_only(true)
@ -226,7 +226,7 @@ pub async fn subscriptions(req: Request<Body>) -> Result<Response<Body>, String>
);
}
Ok(res)
Ok(response)
}
pub async fn wiki(req: Request<Body>) -> Result<Response<Body>, String> {
@ -260,6 +260,7 @@ pub async fn wiki(req: Request<Body>) -> Result<Response<Body>, String> {
pub async fn sidebar(req: Request<Body>) -> Result<Response<Body>, String> {
let sub = req.param("sub").unwrap_or_else(|| "reddit.com".to_string());
let quarantined = can_access_quarantine(&req, &sub);
// Handle random subreddits
if let Ok(random) = catch_random(&sub, "/about/sidebar").await {
return Ok(random);
@ -272,11 +273,12 @@ pub async fn sidebar(req: Request<Body>) -> Result<Response<Body>, String> {
match json(path, quarantined).await {
// If success, receive JSON in response
Ok(response) => template(WikiTemplate {
wiki: format!(
"{}<hr><h1>Moderators</h1><br><ul>{}</ul>",
rewrite_urls(&val(&response, "description_html").replace("\\", "")),
moderators(&sub, quarantined).await?.join(""),
),
wiki: rewrite_urls(&val(&response, "description_html").replace("\\", "")),
// wiki: format!(
// "{}<hr><h1>Moderators</h1><br><ul>{}</ul>",
// rewrite_urls(&val(&response, "description_html").replace("\\", "")),
// moderators(&sub, quarantined).await.unwrap_or(vec!["Could not fetch moderators".to_string()]).join(""),
// ),
sub,
page: "Sidebar".to_string(),
prefs: Preferences::new(req),
@ -291,40 +293,39 @@ pub async fn sidebar(req: Request<Body>) -> Result<Response<Body>, String> {
}
}
pub async fn moderators(sub: &str, quarantined: bool) -> Result<Vec<String>, String> {
// Retrieve and format the html for the moderators list
Ok(
moderators_list(sub, quarantined)
.await?
.iter()
.map(|m| format!("<li><a style=\"color: var(--accent)\" href=\"/u/{name}\">{name}</a></li>", name = m))
.collect(),
)
}
// pub async fn moderators(sub: &str, quarantined: bool) -> Result<Vec<String>, String> {
// // Retrieve and format the html for the moderators list
// Ok(
// moderators_list(sub, quarantined)
// .await?
// .iter()
// .map(|m| format!("<li><a style=\"color: var(--accent)\" href=\"/u/{name}\">{name}</a></li>", name = m))
// .collect(),
// )
// }
async fn moderators_list(sub: &str, quarantined: bool) -> Result<Vec<String>, String> {
// Build the moderator list URL
let path: String = format!("/r/{}/about/moderators.json?raw_json=1", sub);
// async fn moderators_list(sub: &str, quarantined: bool) -> Result<Vec<String>, String> {
// // Build the moderator list URL
// let path: String = format!("/r/{}/about/moderators.json?raw_json=1", sub);
// Retrieve response
let response = json(path, quarantined).await?["data"]["children"].clone();
Ok(
// Traverse json tree and format into list of strings
response
.as_array()
.unwrap_or(&Vec::new())
.iter()
.filter_map(|moderator| {
let name = moderator["name"].as_str().unwrap_or_default();
if name.is_empty() {
None
} else {
Some(name.to_string())
}
})
.collect::<Vec<_>>(),
)
}
// // Retrieve response
// json(path, quarantined).await.map(|response| {
// // Traverse json tree and format into list of strings
// response["data"]["children"]
// .as_array()
// .unwrap_or(&Vec::new())
// .iter()
// .filter_map(|moderator| {
// let name = moderator["name"].as_str().unwrap_or_default();
// if name.is_empty() {
// None
// } else {
// Some(name.to_string())
// }
// })
// .collect::<Vec<_>>()
// })
// }
// SUBREDDIT
async fn subreddit(sub: &str, quarantined: bool) -> Result<Subreddit, String> {
@ -332,32 +333,25 @@ async fn subreddit(sub: &str, quarantined: bool) -> Result<Subreddit, String> {
let path: String = format!("/r/{}/about.json?raw_json=1", sub);
// Send a request to the url
match json(path, quarantined).await {
// If success, receive JSON in response
Ok(res) => {
// Metadata regarding the subreddit
let members: i64 = res["data"]["subscribers"].as_u64().unwrap_or_default() as i64;
let active: i64 = res["data"]["accounts_active"].as_u64().unwrap_or_default() as i64;
let res = json(path, quarantined).await?;
// Fetch subreddit icon either from the community_icon or icon_img value
let community_icon: &str = res["data"]["community_icon"].as_str().unwrap_or_default();
let icon = if community_icon.is_empty() { val(&res, "icon_img") } else { community_icon.to_string() };
// Metadata regarding the subreddit
let members: i64 = res["data"]["subscribers"].as_u64().unwrap_or_default() as i64;
let active: i64 = res["data"]["accounts_active"].as_u64().unwrap_or_default() as i64;
let sub = Subreddit {
name: esc!(&res, "display_name"),
title: esc!(&res, "title"),
description: esc!(&res, "public_description"),
info: rewrite_urls(&val(&res, "description_html").replace("\\", "")),
moderators: moderators_list(sub, quarantined).await?,
icon: format_url(&icon),
members: format_num(members),
active: format_num(active),
wiki: res["data"]["wiki_enabled"].as_bool().unwrap_or_default(),
};
// Fetch subreddit icon either from the community_icon or icon_img value
let community_icon: &str = res["data"]["community_icon"].as_str().unwrap_or_default();
let icon = if community_icon.is_empty() { val(&res, "icon_img") } else { community_icon.to_string() };
Ok(sub)
}
// If the Reddit API returns an error, exit this function
Err(msg) => return Err(msg),
}
Ok(Subreddit {
name: esc!(&res, "display_name"),
title: esc!(&res, "title"),
description: esc!(&res, "public_description"),
info: rewrite_urls(&val(&res, "description_html").replace("\\", "")),
// moderators: moderators_list(sub, quarantined).await.unwrap_or_default(),
icon: format_url(&icon),
members: format_num(members),
active: format_num(active),
wiki: res["data"]["wiki_enabled"].as_bool().unwrap_or_default(),
})
}

View File

@ -61,27 +61,22 @@ async fn user(name: &str) -> Result<User, String> {
let path: String = format!("/user/{}/about.json?raw_json=1", name);
// Send a request to the url
match json(path, false).await {
// If success, receive JSON in response
Ok(res) => {
// Grab creation date as unix timestamp
let created: i64 = res["data"]["created"].as_f64().unwrap_or(0.0).round() as i64;
json(path, false).await.map(|res| {
// Grab creation date as unix timestamp
let created: i64 = res["data"]["created"].as_f64().unwrap_or(0.0).round() as i64;
// Closure used to parse JSON from Reddit APIs
let about = |item| res["data"]["subreddit"][item].as_str().unwrap_or_default().to_string();
// Closure used to parse JSON from Reddit APIs
let about = |item| res["data"]["subreddit"][item].as_str().unwrap_or_default().to_string();
// Parse the JSON output into a User struct
Ok(User {
name: name.to_string(),
title: esc!(about("title")),
icon: format_url(&about("icon_img")),
karma: res["data"]["total_karma"].as_i64().unwrap_or(0),
created: OffsetDateTime::from_unix_timestamp(created).format("%b %d '%y"),
banner: esc!(about("banner_img")),
description: about("public_description"),
})
// Parse the JSON output into a User struct
User {
name: name.to_string(),
title: esc!(about("title")),
icon: format_url(&about("icon_img")),
karma: res["data"]["total_karma"].as_i64().unwrap_or(0),
created: OffsetDateTime::from_unix_timestamp(created).format("%b %d '%y"),
banner: esc!(about("banner_img")),
description: about("public_description"),
}
// If the Reddit API returns an error, exit this function
Err(msg) => return Err(msg),
}
})
}

View File

@ -253,7 +253,7 @@ impl Post {
posts.push(Self {
id: val(post, "id"),
title: esc!(if title.is_empty() { fallback_title.to_owned() } else { title }),
title: esc!(if title.is_empty() { fallback_title.clone() } else { title }),
community: val(post, "subreddit"),
body: rewrite_urls(&val(post, "body_html")),
author: Author {
@ -362,7 +362,7 @@ pub struct Subreddit {
pub title: String,
pub description: String,
pub info: String,
pub moderators: Vec<String>,
// pub moderators: Vec<String>,
pub icon: String,
pub members: (String, String),
pub active: (String, String),
@ -424,7 +424,7 @@ pub fn param(path: &str, value: &str) -> Option<String> {
.into_owned()
.collect::<HashMap<_, _>>()
.get(value)?
.to_owned(),
.clone(),
)
}
@ -463,83 +463,71 @@ pub fn format_url(url: &str) -> String {
if url.is_empty() || url == "self" || url == "default" || url == "nsfw" || url == "spoiler" {
String::new()
} else {
match Url::parse(url) {
Ok(parsed) => {
let domain = parsed.domain().unwrap_or_default();
Url::parse(url).map_or(String::new(), |parsed| {
let domain = parsed.domain().unwrap_or_default();
let capture = |regex: &str, format: &str, segments: i16| {
Regex::new(regex)
.map(|re| match re.captures(url) {
Some(caps) => match segments {
1 => [format, &caps[1]].join(""),
2 => [format, &caps[1], "/", &caps[2]].join(""),
_ => String::new(),
},
None => String::new(),
})
.unwrap_or_default()
let capture = |regex: &str, format: &str, segments: i16| {
Regex::new(regex).map_or(String::new(), |re| {
re.captures(url).map_or(String::new(), |caps| match segments {
1 => [format, &caps[1]].join(""),
2 => [format, &caps[1], "/", &caps[2]].join(""),
_ => String::new(),
})
})
};
macro_rules! chain {
() => {
{
String::new()
}
};
macro_rules! chain {
() => {
{
String::new()
( $first_fn:expr, $($other_fns:expr), *) => {
{
let result = $first_fn;
if result.is_empty() {
chain!($($other_fns,)*)
}
};
( $first_fn:expr, $($other_fns:expr), *) => {
else
{
let result = $first_fn;
if result.is_empty() {
chain!($($other_fns,)*)
}
else
{
result
}
result
}
};
}
match domain {
"v.redd.it" => chain!(
capture(r"https://v\.redd\.it/(.*)/DASH_([0-9]{2,4}(\.mp4|$))", "/vid/", 2),
capture(r"https://v\.redd\.it/(.+)/(HLSPlaylist\.m3u8.*)$", "/hls/", 2)
),
"i.redd.it" => capture(r"https://i\.redd\.it/(.*)", "/img/", 1),
"a.thumbs.redditmedia.com" => capture(r"https://a\.thumbs\.redditmedia\.com/(.*)", "/thumb/a/", 1),
"b.thumbs.redditmedia.com" => capture(r"https://b\.thumbs\.redditmedia\.com/(.*)", "/thumb/b/", 1),
"emoji.redditmedia.com" => capture(r"https://emoji\.redditmedia\.com/(.*)/(.*)", "/emoji/", 2),
"preview.redd.it" => capture(r"https://preview\.redd\.it/(.*)", "/preview/pre/", 1),
"external-preview.redd.it" => capture(r"https://external\-preview\.redd\.it/(.*)", "/preview/external-pre/", 1),
"styles.redditmedia.com" => capture(r"https://styles\.redditmedia\.com/(.*)", "/style/", 1),
"www.redditstatic.com" => capture(r"https://www\.redditstatic\.com/(.*)", "/static/", 1),
_ => String::new(),
}
}
};
}
Err(_) => String::new(),
}
match domain {
"v.redd.it" => chain!(
capture(r"https://v\.redd\.it/(.*)/DASH_([0-9]{2,4}(\.mp4|$))", "/vid/", 2),
capture(r"https://v\.redd\.it/(.+)/(HLSPlaylist\.m3u8.*)$", "/hls/", 2)
),
"i.redd.it" => capture(r"https://i\.redd\.it/(.*)", "/img/", 1),
"a.thumbs.redditmedia.com" => capture(r"https://a\.thumbs\.redditmedia\.com/(.*)", "/thumb/a/", 1),
"b.thumbs.redditmedia.com" => capture(r"https://b\.thumbs\.redditmedia\.com/(.*)", "/thumb/b/", 1),
"emoji.redditmedia.com" => capture(r"https://emoji\.redditmedia\.com/(.*)/(.*)", "/emoji/", 2),
"preview.redd.it" => capture(r"https://preview\.redd\.it/(.*)", "/preview/pre/", 1),
"external-preview.redd.it" => capture(r"https://external\-preview\.redd\.it/(.*)", "/preview/external-pre/", 1),
"styles.redditmedia.com" => capture(r"https://styles\.redditmedia\.com/(.*)", "/style/", 1),
"www.redditstatic.com" => capture(r"https://www\.redditstatic\.com/(.*)", "/static/", 1),
_ => String::new(),
}
})
}
}
// Rewrite Reddit links to Libreddit in body of text
pub fn rewrite_urls(input_text: &str) -> String {
let text1 = match Regex::new(r#"href="(https|http|)://(www.|old.|np.|amp.|)(reddit).(com)/"#) {
Ok(re) => re.replace_all(input_text, r#"href="/"#).to_string(),
Err(_) => String::new(),
};
let text1 = Regex::new(r#"href="(https|http|)://(www.|old.|np.|amp.|)(reddit).(com)/"#).map_or(String::new(), |re| re.replace_all(input_text, r#"href="/"#).to_string());
// Rewrite external media previews to Libreddit
match Regex::new(r"https://external-preview\.redd\.it(.*)[^?]") {
Ok(re) => {
if re.is_match(&text1) {
re.replace_all(&text1, format_url(re.find(&text1).map(|x| x.as_str()).unwrap_or_default())).to_string()
} else {
text1
}
Regex::new(r"https://external-preview\.redd\.it(.*)[^?]").map_or(String::new(), |re| {
if re.is_match(&text1) {
re.replace_all(&text1, format_url(re.find(&text1).map(|x| x.as_str()).unwrap_or_default())).to_string()
} else {
text1
}
Err(_) => String::new(),
}
})
}
// Append `m` and `k` for millions and thousands respectively

View File

@ -156,7 +156,7 @@ html, body, div, h1, h2, h3, h4, h5, h6, ul, ol, dl, li, dt, dd, p, blockquote,
pre, form, fieldset, table, th, td, select, input {
margin: 0;
color: var(--text);
font-family: "Inter";
font-family: "Inter", sans-serif;
}
body {
@ -968,6 +968,7 @@ a.search_subreddit:hover {
font-weight: normal;
padding: 5px 5px;
margin: 5px 0;
overflow: auto;
}
.comment_body.highlighted {
@ -1195,7 +1196,7 @@ input[type="submit"] {
}
.md code {
font-family: monospace;
font-family: monospace, sans-serif;
font-size: 14px;
}

View File

@ -68,6 +68,10 @@
</div>
{% endif %}
{% endfor %}
{% if prefs.use_hls == "on" %}
<script src="/hls.min.js"></script>
<script src="/playHLSVideo.js"></script>
{% endif %}
<footer>
{% if params.before != "" %}

View File

@ -105,14 +105,14 @@
<summary id="sidebar_label">Sidebar</summary>
<div id="sidebar_contents">
{{ sub.info }}
<hr>
{# <hr>
<h2>Moderators</h2>
<br>
<ul>
{% for moderator in sub.moderators %}
<li><a style="color: var(--accent)" href="/u/{{ moderator }}">{{ moderator }}</a></li>
{% endfor %}
</ul>
</ul> #}
</div>
</details>
</aside>

View File

@ -49,8 +49,11 @@
</details>
</div>
{% endif %}
{% endfor %}
{% if prefs.use_hls == "on" %}
<script src="/hls.min.js"></script>
<script src="/playHLSVideo.js"></script>
{% endif %}
</div>
<footer>