Optimizations and commenting
This commit is contained in:
parent
ba42fc066f
commit
4defb58f2a
6
Cargo.lock
generated
6
Cargo.lock
generated
@ -609,7 +609,7 @@ checksum = "18794a8ad5b29321f790b55d93dfba91e125cb1a9edbd4f8e3150acc771c1a5e"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libreddit"
|
name = "libreddit"
|
||||||
version = "0.14.3"
|
version = "0.14.4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"askama",
|
"askama",
|
||||||
"async-recursion",
|
"async-recursion",
|
||||||
@ -807,9 +807,9 @@ checksum = "bc881b2c22681370c6a780e47af9840ef841837bc98118431d4e1868bd0c1086"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.26"
|
version = "1.0.27"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a152013215dca273577e18d2bf00fa862b89b24169fb78c4c95aeb07992c9cec"
|
checksum = "f0d8caf72986c1a598726adc988bb5984792ef84f5ee5aa50209145ee8077038"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"unicode-xid",
|
"unicode-xid",
|
||||||
]
|
]
|
||||||
|
@ -3,7 +3,7 @@ name = "libreddit"
|
|||||||
description = " Alternative private front-end to Reddit"
|
description = " Alternative private front-end to Reddit"
|
||||||
license = "AGPL-3.0"
|
license = "AGPL-3.0"
|
||||||
repository = "https://github.com/spikecodes/libreddit"
|
repository = "https://github.com/spikecodes/libreddit"
|
||||||
version = "0.14.3"
|
version = "0.14.4"
|
||||||
authors = ["spikecodes <19519553+spikecodes@users.noreply.github.com>"]
|
authors = ["spikecodes <19519553+spikecodes@users.noreply.github.com>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|
||||||
|
@ -9,7 +9,9 @@ use crate::server::RequestExt;
|
|||||||
pub async fn proxy(req: Request<Body>, format: &str) -> Result<Response<Body>, String> {
|
pub async fn proxy(req: Request<Body>, format: &str) -> Result<Response<Body>, String> {
|
||||||
let mut url = format!("{}?{}", format, req.uri().query().unwrap_or_default());
|
let mut url = format!("{}?{}", format, req.uri().query().unwrap_or_default());
|
||||||
|
|
||||||
|
// For each parameter in request
|
||||||
for (name, value) in req.params().iter() {
|
for (name, value) in req.params().iter() {
|
||||||
|
// Fill the parameter value in the url
|
||||||
url = url.replace(&format!("{{{}}}", name), value);
|
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);
|
let mut builder = Request::get(url);
|
||||||
|
|
||||||
// Copy useful headers from original request
|
// Copy useful headers from original request
|
||||||
let headers = req.headers();
|
|
||||||
for &key in &["Range", "If-Modified-Since", "Cache-Control"] {
|
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);
|
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
|
client
|
||||||
.request(stream_request)
|
.request(stream_request)
|
||||||
@ -64,9 +65,10 @@ fn request(url: String, quarantine: bool) -> Boxed<Result<Response<Body>, String
|
|||||||
// Prepare the HTTPS connector.
|
// Prepare the HTTPS connector.
|
||||||
let https = hyper_rustls::HttpsConnector::with_native_roots();
|
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);
|
let client: client::Client<_, hyper::Body> = client::Client::builder().build(https);
|
||||||
|
|
||||||
|
// Build request
|
||||||
let builder = Request::builder()
|
let builder = Request::builder()
|
||||||
.method("GET")
|
.method("GET")
|
||||||
.uri(&url)
|
.uri(&url)
|
||||||
|
11
src/main.rs
11
src/main.rs
@ -1,14 +1,7 @@
|
|||||||
// Global specifiers
|
// Global specifiers
|
||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
#![warn(clippy::pedantic, clippy::all)]
|
#![warn(clippy::pedantic, clippy::all)]
|
||||||
#![allow(
|
#![allow(clippy::needless_pass_by_value, clippy::cast_possible_truncation, clippy::cast_possible_wrap, clippy::find_map)]
|
||||||
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
|
|
||||||
)]
|
|
||||||
|
|
||||||
// Reference local files
|
// Reference local files
|
||||||
mod post;
|
mod post;
|
||||||
@ -137,7 +130,7 @@ async fn main() {
|
|||||||
let port = matches.value_of("port").unwrap_or("8080");
|
let port = matches.value_of("port").unwrap_or("8080");
|
||||||
let hsts = matches.value_of("hsts");
|
let hsts = matches.value_of("hsts");
|
||||||
|
|
||||||
let listener = format!("{}:{}", address, port);
|
let listener = [address, ":", port].concat();
|
||||||
|
|
||||||
println!("Starting Libreddit...");
|
println!("Starting Libreddit...");
|
||||||
|
|
||||||
|
144
src/post.rs
144
src/post.rs
@ -7,8 +7,6 @@ use crate::utils::{error, format_num, format_url, param, rewrite_urls, setting,
|
|||||||
|
|
||||||
use hyper::{Body, Request, Response};
|
use hyper::{Body, Request, Response};
|
||||||
|
|
||||||
use async_recursion::async_recursion;
|
|
||||||
|
|
||||||
use askama::Template;
|
use askama::Template;
|
||||||
|
|
||||||
// STRUCTS
|
// 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
|
// Send a request to the url, receive JSON in response
|
||||||
match json(path, quarantined).await {
|
match json(path, quarantined).await {
|
||||||
// Otherwise, grab the JSON output from the request
|
// Otherwise, grab the JSON output from the request
|
||||||
Ok(res) => {
|
Ok(response) => {
|
||||||
// Parse the JSON into Post and Comment structs
|
// Parse the JSON into Post and Comment structs
|
||||||
let post = parse_post(&res[0]).await;
|
let post = parse_post(&response[0]).await;
|
||||||
let comments = parse_comments(&res[1], &post.permalink, &post.author.name, highlighted_comment).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
|
// Use the Post and Comment structs to generate a website to show users
|
||||||
template(PostTemplate {
|
template(PostTemplate {
|
||||||
@ -151,79 +149,71 @@ async fn parse_post(json: &serde_json::Value) -> Post {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// COMMENTS
|
// COMMENTS
|
||||||
#[async_recursion]
|
fn parse_comments(json: &serde_json::Value, post_link: &str, post_author: &str, highlighted_comment: &str) -> Vec<Comment> {
|
||||||
async 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
|
||||||
// Separate the comment JSON into a Vector of comments
|
let comments = json["data"]["children"].as_array().map_or(Vec::new(), std::borrow::ToOwned::to_owned);
|
||||||
let comment_data = match json["data"]["children"].as_array() {
|
|
||||||
Some(f) => f.to_owned(),
|
|
||||||
None => Vec::new(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut comments: Vec<Comment> = Vec::new();
|
|
||||||
|
|
||||||
// For each comment, retrieve the values to build a Comment object
|
// 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
|
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()
|
||||||
}
|
}
|
||||||
|
@ -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 sort = param(&path, "sort").unwrap_or_else(|| "relevance".to_string());
|
||||||
|
|
||||||
let subreddits = match param(&path, "restrict_sr") {
|
// If search is not restricted to this subreddit, show other subreddits in search results
|
||||||
None => search_subreddits(&query).await,
|
let subreddits = param(&path, "restrict_sr").map_or(search_subreddits(&query).await, |_| Vec::new());
|
||||||
Some(_) => Vec::new(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let url = String::from(req.uri().path_and_query().map_or("", |val| val.as_str()));
|
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(' ', "+"));
|
let subreddit_search_path = format!("/subreddits/search.json?q={}&limit=3", q.replace(' ', "+"));
|
||||||
|
|
||||||
// Send a request to the url
|
// Send a request to the url
|
||||||
match json(subreddit_search_path, false).await {
|
json(subreddit_search_path, false).await.unwrap_or_default()["data"]["children"]
|
||||||
// If success, receive JSON in response
|
.as_array()
|
||||||
Ok(response) => {
|
.map(ToOwned::to_owned)
|
||||||
match response["data"]["children"].as_array() {
|
.unwrap_or_default()
|
||||||
// For each subreddit from subreddit list
|
.iter()
|
||||||
Some(list) => list
|
.map(|subreddit| {
|
||||||
.iter()
|
// For each subreddit from subreddit list
|
||||||
.map(|subreddit| {
|
// Fetch subreddit icon either from the community_icon or icon_img value
|
||||||
// Fetch subreddit icon either from the community_icon or icon_img value
|
let icon = subreddit["data"]["community_icon"]
|
||||||
let community_icon: &str = subreddit["data"]["community_icon"].as_str().map_or("", |s| s.split('?').collect::<Vec<&str>>()[0]);
|
.as_str()
|
||||||
let icon = if community_icon.is_empty() {
|
.map_or_else(|| val(&subreddit, "icon_img"), ToString::to_string);
|
||||||
val(&subreddit, "icon_img")
|
|
||||||
} else {
|
|
||||||
community_icon.to_string()
|
|
||||||
};
|
|
||||||
|
|
||||||
Subreddit {
|
Subreddit {
|
||||||
name: val(subreddit, "display_name_prefixed"),
|
name: val(subreddit, "display_name_prefixed"),
|
||||||
url: val(subreddit, "url"),
|
url: val(subreddit, "url"),
|
||||||
icon: format_url(&icon),
|
icon: format_url(&icon),
|
||||||
description: val(subreddit, "public_description"),
|
description: val(subreddit, "public_description"),
|
||||||
subscribers: format_num(subreddit["data"]["subscribers"].as_f64().unwrap_or_default() as i64),
|
subscribers: format_num(subreddit["data"]["subscribers"].as_f64().unwrap_or_default() as i64),
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect::<Vec<Subreddit>>(),
|
|
||||||
_ => Vec::new(),
|
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
// If the Reddit API returns an error, exit this function
|
.collect::<Vec<Subreddit>>()
|
||||||
_ => Vec::new(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -69,29 +69,31 @@ impl RequestExt for Request<Body> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn cookies(&self) -> Vec<Cookie> {
|
fn cookies(&self) -> Vec<Cookie> {
|
||||||
let mut cookies = Vec::new();
|
self.headers().get("Cookie").map_or(Vec::new(), |header| {
|
||||||
if let Some(header) = self.headers().get("Cookie") {
|
header
|
||||||
for cookie in header.to_str().unwrap_or_default().split("; ") {
|
.to_str()
|
||||||
cookies.push(Cookie::parse(cookie).unwrap_or_else(|_| Cookie::named("")));
|
.unwrap_or_default()
|
||||||
}
|
.split("; ")
|
||||||
}
|
.map(|cookie| Cookie::parse(cookie).unwrap_or_else(|_| Cookie::named("")))
|
||||||
cookies
|
.collect()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cookie(&self, name: &str) -> Option<Cookie> {
|
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> {
|
impl ResponseExt for Response<Body> {
|
||||||
fn cookies(&self) -> Vec<Cookie> {
|
fn cookies(&self) -> Vec<Cookie> {
|
||||||
let mut cookies = Vec::new();
|
self.headers().get("Cookie").map_or(Vec::new(), |header| {
|
||||||
for header in self.headers().get_all("Cookie") {
|
header
|
||||||
if let Ok(cookie) = Cookie::parse(header.to_str().unwrap_or_default()) {
|
.to_str()
|
||||||
cookies.push(cookie);
|
.unwrap_or_default()
|
||||||
}
|
.split("; ")
|
||||||
}
|
.map(|cookie| Cookie::parse(cookie).unwrap_or_else(|_| Cookie::named("")))
|
||||||
cookies
|
.collect()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn insert_cookie(&mut self, cookie: Cookie) {
|
fn insert_cookie(&mut self, cookie: Cookie) {
|
||||||
@ -144,6 +146,7 @@ impl Server {
|
|||||||
|
|
||||||
pub fn listen(self, addr: String) -> Boxed<Result<(), hyper::Error>> {
|
pub fn listen(self, addr: String) -> Boxed<Result<(), hyper::Error>> {
|
||||||
let make_svc = make_service_fn(move |_conn| {
|
let make_svc = make_service_fn(move |_conn| {
|
||||||
|
// For correct borrowing, these values need to be borrowed
|
||||||
let router = self.router.clone();
|
let router = self.router.clone();
|
||||||
let default_headers = self.default_headers.clone();
|
let default_headers = self.default_headers.clone();
|
||||||
|
|
||||||
@ -159,7 +162,7 @@ impl Server {
|
|||||||
let mut path = req.uri().path().replace("//", "/");
|
let mut path = req.uri().path().replace("//", "/");
|
||||||
|
|
||||||
// Remove trailing slashes
|
// Remove trailing slashes
|
||||||
if path.ends_with('/') && path != "/" {
|
if path != "/" && path.ends_with('/') {
|
||||||
path.pop();
|
path.pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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 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());
|
server.boxed()
|
||||||
|
|
||||||
graceful.boxed()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn shutdown_signal() {
|
|
||||||
// Wait for the CTRL+C signal
|
|
||||||
tokio::signal::ctrl_c().await.expect("Failed to install CTRL+C signal handler");
|
|
||||||
}
|
|
||||||
|
@ -43,12 +43,12 @@ pub async fn set(req: Request<Body>) -> Result<Response<Body>, String> {
|
|||||||
let (parts, mut body) = req.into_parts();
|
let (parts, mut body) = req.into_parts();
|
||||||
|
|
||||||
// Grab existing cookies
|
// Grab existing cookies
|
||||||
let mut cookies = Vec::new();
|
let _cookies: Vec<Cookie> = parts
|
||||||
for header in parts.headers.get_all("Cookie") {
|
.headers
|
||||||
if let Ok(cookie) = Cookie::parse(header.to_str().unwrap_or_default()) {
|
.get_all("Cookie")
|
||||||
cookies.push(cookie);
|
.iter()
|
||||||
}
|
.filter_map(|header| Cookie::parse(header.to_str().unwrap_or_default()).ok())
|
||||||
}
|
.collect();
|
||||||
|
|
||||||
// Aggregate the body...
|
// Aggregate the body...
|
||||||
// let whole_body = hyper::body::aggregate(req).await.map_err(|e| e.to_string())?;
|
// 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 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 {
|
for &name in &PREFS {
|
||||||
match form.get(name) {
|
match form.get(name) {
|
||||||
Some(value) => res.insert_cookie(
|
Some(value) => response.insert_cookie(
|
||||||
Cookie::build(name.to_owned(), value.to_owned())
|
Cookie::build(name.to_owned(), value.to_owned())
|
||||||
.path("/")
|
.path("/")
|
||||||
.http_only(true)
|
.http_only(true)
|
||||||
.expires(OffsetDateTime::now_utc() + Duration::weeks(52))
|
.expires(OffsetDateTime::now_utc() + Duration::weeks(52))
|
||||||
.finish(),
|
.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> {
|
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();
|
let (parts, _) = req.into_parts();
|
||||||
|
|
||||||
// Grab existing cookies
|
// Grab existing cookies
|
||||||
let mut cookies = Vec::new();
|
let _cookies: Vec<Cookie> = parts
|
||||||
for header in parts.headers.get_all("Cookie") {
|
.headers
|
||||||
if let Ok(cookie) = Cookie::parse(header.to_str().unwrap_or_default()) {
|
.get_all("Cookie")
|
||||||
cookies.push(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();
|
let query = parts.uri.query().unwrap_or_default().as_bytes();
|
||||||
|
|
||||||
@ -101,11 +101,11 @@ fn set_cookies_method(req: Request<Body>, remove_cookies: bool) -> Response<Body
|
|||||||
None => "/".to_string(),
|
None => "/".to_string(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut res = redirect(path);
|
let mut response = redirect(path);
|
||||||
|
|
||||||
for name in [PREFS.to_vec(), vec!["subscriptions"]].concat() {
|
for name in [PREFS.to_vec(), vec!["subscriptions"]].concat() {
|
||||||
match form.get(name) {
|
match form.get(name) {
|
||||||
Some(value) => res.insert_cookie(
|
Some(value) => response.insert_cookie(
|
||||||
Cookie::build(name.to_owned(), value.to_owned())
|
Cookie::build(name.to_owned(), value.to_owned())
|
||||||
.path("/")
|
.path("/")
|
||||||
.http_only(true)
|
.http_only(true)
|
||||||
@ -114,13 +114,13 @@ fn set_cookies_method(req: Request<Body>, remove_cookies: bool) -> Response<Body
|
|||||||
),
|
),
|
||||||
None => {
|
None => {
|
||||||
if remove_cookies {
|
if remove_cookies {
|
||||||
res.remove_cookie(name.to_string())
|
response.remove_cookie(name.to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
res
|
response
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set cookies using response "Set-Cookie" header
|
// Set cookies using response "Set-Cookie" header
|
||||||
|
@ -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> {
|
pub async fn add_quarantine_exception(req: Request<Body>) -> Result<Response<Body>, String> {
|
||||||
let subreddit = req.param("sub").ok_or("Invalid URL")?;
|
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 redir = param(&format!("?{}", req.uri().query().unwrap_or_default()), "redir").ok_or("Invalid URL")?;
|
||||||
let mut res = redirect(redir);
|
let mut response = redirect(redir);
|
||||||
res.insert_cookie(
|
response.insert_cookie(
|
||||||
Cookie::build(&format!("allow_quaran_{}", subreddit.to_lowercase()), "true")
|
Cookie::build(&format!("allow_quaran_{}", subreddit.to_lowercase()), "true")
|
||||||
.path("/")
|
.path("/")
|
||||||
.http_only(true)
|
.http_only(true)
|
||||||
.expires(cookie::Expiration::Session)
|
.expires(cookie::Expiration::Session)
|
||||||
.finish(),
|
.finish(),
|
||||||
);
|
);
|
||||||
Ok(res)
|
Ok(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn can_access_quarantine(req: &Request<Body>, sub: &str) -> bool {
|
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)
|
format!("/r/{}", sub)
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut res = redirect(path);
|
let mut response = redirect(path);
|
||||||
|
|
||||||
// Delete cookie if empty, else set
|
// Delete cookie if empty, else set
|
||||||
if sub_list.is_empty() {
|
if sub_list.is_empty() {
|
||||||
res.remove_cookie("subscriptions".to_string());
|
response.remove_cookie("subscriptions".to_string());
|
||||||
} else {
|
} else {
|
||||||
res.insert_cookie(
|
response.insert_cookie(
|
||||||
Cookie::build("subscriptions", sub_list.join("+"))
|
Cookie::build("subscriptions", sub_list.join("+"))
|
||||||
.path("/")
|
.path("/")
|
||||||
.http_only(true)
|
.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> {
|
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> {
|
pub async fn sidebar(req: Request<Body>) -> Result<Response<Body>, String> {
|
||||||
let sub = req.param("sub").unwrap_or_else(|| "reddit.com".to_string());
|
let sub = req.param("sub").unwrap_or_else(|| "reddit.com".to_string());
|
||||||
let quarantined = can_access_quarantine(&req, &sub);
|
let quarantined = can_access_quarantine(&req, &sub);
|
||||||
|
|
||||||
// Handle random subreddits
|
// Handle random subreddits
|
||||||
if let Ok(random) = catch_random(&sub, "/about/sidebar").await {
|
if let Ok(random) = catch_random(&sub, "/about/sidebar").await {
|
||||||
return Ok(random);
|
return Ok(random);
|
||||||
@ -307,10 +308,9 @@ async fn moderators_list(sub: &str, quarantined: bool) -> Result<Vec<String>, St
|
|||||||
let path: String = format!("/r/{}/about/moderators.json?raw_json=1", sub);
|
let path: String = format!("/r/{}/about/moderators.json?raw_json=1", sub);
|
||||||
|
|
||||||
// Retrieve response
|
// Retrieve response
|
||||||
let response = json(path, quarantined).await?["data"]["children"].clone();
|
json(path, quarantined).await.map(|response| {
|
||||||
Ok(
|
|
||||||
// Traverse json tree and format into list of strings
|
// Traverse json tree and format into list of strings
|
||||||
response
|
response["data"]["children"]
|
||||||
.as_array()
|
.as_array()
|
||||||
.unwrap_or(&Vec::new())
|
.unwrap_or(&Vec::new())
|
||||||
.iter()
|
.iter()
|
||||||
@ -322,8 +322,8 @@ async fn moderators_list(sub: &str, quarantined: bool) -> Result<Vec<String>, St
|
|||||||
Some(name.to_string())
|
Some(name.to_string())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>(),
|
.collect::<Vec<_>>()
|
||||||
)
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// SUBREDDIT
|
// SUBREDDIT
|
||||||
@ -332,32 +332,25 @@ async fn subreddit(sub: &str, quarantined: bool) -> Result<Subreddit, String> {
|
|||||||
let path: String = format!("/r/{}/about.json?raw_json=1", sub);
|
let path: String = format!("/r/{}/about.json?raw_json=1", sub);
|
||||||
|
|
||||||
// Send a request to the url
|
// Send a request to the url
|
||||||
match json(path, quarantined).await {
|
let res = 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;
|
|
||||||
|
|
||||||
// Fetch subreddit icon either from the community_icon or icon_img value
|
// Metadata regarding the subreddit
|
||||||
let community_icon: &str = res["data"]["community_icon"].as_str().unwrap_or_default();
|
let members: i64 = res["data"]["subscribers"].as_u64().unwrap_or_default() as i64;
|
||||||
let icon = if community_icon.is_empty() { val(&res, "icon_img") } else { community_icon.to_string() };
|
let active: i64 = res["data"]["accounts_active"].as_u64().unwrap_or_default() as i64;
|
||||||
|
|
||||||
let sub = Subreddit {
|
// Fetch subreddit icon either from the community_icon or icon_img value
|
||||||
name: esc!(&res, "display_name"),
|
let community_icon: &str = res["data"]["community_icon"].as_str().unwrap_or_default();
|
||||||
title: esc!(&res, "title"),
|
let icon = if community_icon.is_empty() { val(&res, "icon_img") } else { community_icon.to_string() };
|
||||||
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(),
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(sub)
|
Ok(Subreddit {
|
||||||
}
|
name: esc!(&res, "display_name"),
|
||||||
// If the Reddit API returns an error, exit this function
|
title: esc!(&res, "title"),
|
||||||
Err(msg) => return Err(msg),
|
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(),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
35
src/user.rs
35
src/user.rs
@ -61,27 +61,22 @@ async fn user(name: &str) -> Result<User, String> {
|
|||||||
let path: String = format!("/user/{}/about.json?raw_json=1", name);
|
let path: String = format!("/user/{}/about.json?raw_json=1", name);
|
||||||
|
|
||||||
// Send a request to the url
|
// Send a request to the url
|
||||||
match json(path, false).await {
|
json(path, false).await.map(|res| {
|
||||||
// If success, receive JSON in response
|
// Grab creation date as unix timestamp
|
||||||
Ok(res) => {
|
let created: i64 = res["data"]["created"].as_f64().unwrap_or(0.0).round() as i64;
|
||||||
// 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
|
// Closure used to parse JSON from Reddit APIs
|
||||||
let about = |item| res["data"]["subreddit"][item].as_str().unwrap_or_default().to_string();
|
let about = |item| res["data"]["subreddit"][item].as_str().unwrap_or_default().to_string();
|
||||||
|
|
||||||
// Parse the JSON output into a User struct
|
// Parse the JSON output into a User struct
|
||||||
Ok(User {
|
User {
|
||||||
name: name.to_string(),
|
name: name.to_string(),
|
||||||
title: esc!(about("title")),
|
title: esc!(about("title")),
|
||||||
icon: format_url(&about("icon_img")),
|
icon: format_url(&about("icon_img")),
|
||||||
karma: res["data"]["total_karma"].as_i64().unwrap_or(0),
|
karma: res["data"]["total_karma"].as_i64().unwrap_or(0),
|
||||||
created: OffsetDateTime::from_unix_timestamp(created).format("%b %d '%y"),
|
created: OffsetDateTime::from_unix_timestamp(created).format("%b %d '%y"),
|
||||||
banner: esc!(about("banner_img")),
|
banner: esc!(about("banner_img")),
|
||||||
description: about("public_description"),
|
description: about("public_description"),
|
||||||
})
|
|
||||||
}
|
}
|
||||||
// If the Reddit API returns an error, exit this function
|
})
|
||||||
Err(msg) => return Err(msg),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
112
src/utils.rs
112
src/utils.rs
@ -463,83 +463,71 @@ pub fn format_url(url: &str) -> String {
|
|||||||
if url.is_empty() || url == "self" || url == "default" || url == "nsfw" || url == "spoiler" {
|
if url.is_empty() || url == "self" || url == "default" || url == "nsfw" || url == "spoiler" {
|
||||||
String::new()
|
String::new()
|
||||||
} else {
|
} else {
|
||||||
match Url::parse(url) {
|
Url::parse(url).map_or(String::new(), |parsed| {
|
||||||
Ok(parsed) => {
|
let domain = parsed.domain().unwrap_or_default();
|
||||||
let domain = parsed.domain().unwrap_or_default();
|
|
||||||
|
|
||||||
let capture = |regex: &str, format: &str, segments: i16| {
|
let capture = |regex: &str, format: &str, segments: i16| {
|
||||||
Regex::new(regex)
|
Regex::new(regex).map_or(String::new(), |re| {
|
||||||
.map(|re| match re.captures(url) {
|
re.captures(url).map_or(String::new(), |caps| match segments {
|
||||||
Some(caps) => match segments {
|
1 => [format, &caps[1]].join(""),
|
||||||
1 => [format, &caps[1]].join(""),
|
2 => [format, &caps[1], "/", &caps[2]].join(""),
|
||||||
2 => [format, &caps[1], "/", &caps[2]].join(""),
|
_ => String::new(),
|
||||||
_ => String::new(),
|
})
|
||||||
},
|
})
|
||||||
None => String::new(),
|
};
|
||||||
})
|
|
||||||
.unwrap_or_default()
|
macro_rules! chain {
|
||||||
|
() => {
|
||||||
|
{
|
||||||
|
String::new()
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
macro_rules! chain {
|
( $first_fn:expr, $($other_fns:expr), *) => {
|
||||||
() => {
|
{
|
||||||
{
|
let result = $first_fn;
|
||||||
String::new()
|
if result.is_empty() {
|
||||||
|
chain!($($other_fns,)*)
|
||||||
}
|
}
|
||||||
};
|
else
|
||||||
|
|
||||||
( $first_fn:expr, $($other_fns:expr), *) => {
|
|
||||||
{
|
{
|
||||||
let result = $first_fn;
|
result
|
||||||
if result.is_empty() {
|
|
||||||
chain!($($other_fns,)*)
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
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
|
// Rewrite Reddit links to Libreddit in body of text
|
||||||
pub fn rewrite_urls(input_text: &str) -> String {
|
pub fn rewrite_urls(input_text: &str) -> String {
|
||||||
let text1 = match Regex::new(r#"href="(https|http|)://(www.|old.|np.|amp.|)(reddit).(com)/"#) {
|
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());
|
||||||
Ok(re) => re.replace_all(input_text, r#"href="/"#).to_string(),
|
|
||||||
Err(_) => String::new(),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Rewrite external media previews to Libreddit
|
// Rewrite external media previews to Libreddit
|
||||||
match Regex::new(r"https://external-preview\.redd\.it(.*)[^?]") {
|
Regex::new(r"https://external-preview\.redd\.it(.*)[^?]").map_or(String::new(), |re| {
|
||||||
Ok(re) => {
|
if re.is_match(&text1) {
|
||||||
if re.is_match(&text1) {
|
re.replace_all(&text1, format_url(re.find(&text1).map(|x| x.as_str()).unwrap_or_default())).to_string()
|
||||||
re.replace_all(&text1, format_url(re.find(&text1).map(|x| x.as_str()).unwrap_or_default())).to_string()
|
} else {
|
||||||
} else {
|
text1
|
||||||
text1
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Err(_) => String::new(),
|
})
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Append `m` and `k` for millions and thousands respectively
|
// Append `m` and `k` for millions and thousands respectively
|
||||||
|
Loading…
Reference in New Issue
Block a user