Move from Actix Web to Tide (#99)
* Initial commit * Port posts * Pinpoint Tide Bug * Revert testing * Add basic sub support * Unwrap nested routes * Front page & sync templates * Port remaining functions * Log request errors * Clean main and settings * Handle /w/ requests * Create template() util * Reduce caching time to 30s * Fix subscription redirects * Handle frontpage sorting
This commit is contained in:
parent
402b3149e1
commit
ebbdd7185f
1742
Cargo.lock
generated
1742
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
14
Cargo.toml
14
Cargo.toml
@ -8,15 +8,15 @@ authors = ["spikecodes <19519553+spikecodes@users.noreply.github.com>"]
|
|||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
tide = "0.16"
|
||||||
|
async-std = { version = "1", features = ["attributes"] }
|
||||||
|
surf = "2"
|
||||||
base64 = "0.13"
|
base64 = "0.13"
|
||||||
actix-web = { version = "3.3", features = ["rustls"] }
|
cached = "0.23"
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
askama = "0.10"
|
askama = "0.10"
|
||||||
ureq = "2"
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde = { version = "1.0", default_features = false, features = ["derive"] }
|
serde_json = "1"
|
||||||
serde_json = "1.0"
|
|
||||||
async-recursion = "0.3"
|
async-recursion = "0.3"
|
||||||
url = "2.2"
|
regex = "1"
|
||||||
regex = "1.4"
|
|
||||||
time = "0.2"
|
time = "0.2"
|
||||||
cached = "0.23"
|
|
@ -1,4 +1,4 @@
|
|||||||
edition = "2018"
|
edition = "2018"
|
||||||
tab_spaces = 2
|
tab_spaces = 2
|
||||||
hard_tabs = true
|
hard_tabs = true
|
||||||
max_width = 175
|
max_width = 150
|
245
src/main.rs
245
src/main.rs
@ -1,9 +1,7 @@
|
|||||||
// Import Crates
|
// Import Crates
|
||||||
use actix_web::{
|
// use askama::filters::format;
|
||||||
dev::{Service, ServiceResponse},
|
use surf::utils::async_trait;
|
||||||
middleware, web, App, HttpResponse, HttpServer,
|
use tide::{utils::After, Middleware, Next, Request, Response};
|
||||||
};
|
|
||||||
use futures::future::FutureExt;
|
|
||||||
|
|
||||||
// Reference local files
|
// Reference local files
|
||||||
mod post;
|
mod post;
|
||||||
@ -14,43 +12,103 @@ mod subreddit;
|
|||||||
mod user;
|
mod user;
|
||||||
mod utils;
|
mod utils;
|
||||||
|
|
||||||
|
// Build middleware
|
||||||
|
struct HttpsRedirect<HttpsOnly>(HttpsOnly);
|
||||||
|
struct NormalizePath;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<State, HttpsOnly> Middleware<State> for HttpsRedirect<HttpsOnly>
|
||||||
|
where
|
||||||
|
State: Clone + Send + Sync + 'static,
|
||||||
|
HttpsOnly: Into<bool> + Copy + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
async fn handle(&self, request: Request<State>, next: Next<'_, State>) -> tide::Result {
|
||||||
|
let secure = request.url().scheme() == "https";
|
||||||
|
|
||||||
|
if self.0.into() && !secure {
|
||||||
|
let mut secured = request.url().to_owned();
|
||||||
|
secured.set_scheme("https").unwrap_or_default();
|
||||||
|
|
||||||
|
Ok(Response::builder(302).header("Location", secured.to_string()).build())
|
||||||
|
} else {
|
||||||
|
Ok(next.run(request).await)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<State: Clone + Send + Sync + 'static> Middleware<State> for NormalizePath {
|
||||||
|
async fn handle(&self, request: Request<State>, next: Next<'_, State>) -> tide::Result {
|
||||||
|
if !request.url().path().ends_with('/') {
|
||||||
|
Ok(Response::builder(301).header("Location", format!("{}/", request.url().path())).build())
|
||||||
|
} else {
|
||||||
|
Ok(next.run(request).await)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Create Services
|
// Create Services
|
||||||
async fn style() -> HttpResponse {
|
async fn style(_req: Request<()>) -> tide::Result {
|
||||||
HttpResponse::Ok().content_type("text/css").body(include_str!("../static/style.css"))
|
Ok(
|
||||||
|
Response::builder(200)
|
||||||
|
.content_type("text/css")
|
||||||
|
.body(include_str!("../static/style.css"))
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Required for creating a PWA
|
// Required for creating a PWA
|
||||||
async fn manifest() -> HttpResponse {
|
async fn manifest(_req: Request<()>) -> tide::Result {
|
||||||
HttpResponse::Ok().content_type("application/json").body(include_str!("../static/manifest.json"))
|
Ok(
|
||||||
|
Response::builder(200)
|
||||||
|
.content_type("application/json")
|
||||||
|
.body(include_str!("../static/manifest.json"))
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Required for the manifest to be valid
|
// Required for the manifest to be valid
|
||||||
async fn pwa_logo() -> HttpResponse {
|
async fn pwa_logo(_req: Request<()>) -> tide::Result {
|
||||||
HttpResponse::Ok().content_type("image/png").body(include_bytes!("../static/logo.png").as_ref())
|
Ok(
|
||||||
|
Response::builder(200)
|
||||||
|
.content_type("image/png")
|
||||||
|
.body(include_bytes!("../static/logo.png").as_ref())
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Required for iOS App Icons
|
// Required for iOS App Icons
|
||||||
async fn iphone_logo() -> HttpResponse {
|
async fn iphone_logo(_req: Request<()>) -> tide::Result {
|
||||||
HttpResponse::Ok()
|
Ok(
|
||||||
|
Response::builder(200)
|
||||||
.content_type("image/png")
|
.content_type("image/png")
|
||||||
.body(include_bytes!("../static/touch-icon-iphone.png").as_ref())
|
.body(include_bytes!("../static/touch-icon-iphone.png").as_ref())
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn robots() -> HttpResponse {
|
async fn robots(_req: Request<()>) -> tide::Result {
|
||||||
HttpResponse::Ok()
|
Ok(
|
||||||
|
Response::builder(200)
|
||||||
|
.content_type("text/plain")
|
||||||
.header("Cache-Control", "public, max-age=1209600, s-maxage=86400")
|
.header("Cache-Control", "public, max-age=1209600, s-maxage=86400")
|
||||||
.body("User-agent: *\nAllow: /")
|
.body("User-agent: *\nAllow: /")
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn favicon() -> HttpResponse {
|
async fn favicon(_req: Request<()>) -> tide::Result {
|
||||||
HttpResponse::Ok()
|
Ok(
|
||||||
.content_type("image/x-icon")
|
Response::builder(200)
|
||||||
|
.content_type("image/vnd.microsoft.icon")
|
||||||
.header("Cache-Control", "public, max-age=1209600, s-maxage=86400")
|
.header("Cache-Control", "public, max-age=1209600, s-maxage=86400")
|
||||||
.body(include_bytes!("../static/favicon.ico").as_ref())
|
.body(include_bytes!("../static/favicon.ico").as_ref())
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix_web::main]
|
#[async_std::main]
|
||||||
async fn main() -> std::io::Result<()> {
|
async fn main() -> tide::Result<()> {
|
||||||
let mut address = "0.0.0.0:8080".to_string();
|
let mut address = "0.0.0.0:8080".to_string();
|
||||||
let mut force_https = false;
|
let mut force_https = false;
|
||||||
|
|
||||||
@ -62,101 +120,96 @@ async fn main() -> std::io::Result<()> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// start http server
|
// Start HTTP server
|
||||||
println!("Running Libreddit v{} on {}!", env!("CARGO_PKG_VERSION"), &address);
|
println!("Running Libreddit v{} on {}!", env!("CARGO_PKG_VERSION"), &address);
|
||||||
|
|
||||||
HttpServer::new(move || {
|
let mut app = tide::new();
|
||||||
App::new()
|
|
||||||
// Redirect to HTTPS if "--redirect-https" enabled
|
// Redirect to HTTPS if "--redirect-https" enabled
|
||||||
.wrap_fn(move |req, srv| {
|
app.with(HttpsRedirect(force_https));
|
||||||
let secure = req.connection_info().scheme() == "https";
|
|
||||||
let https_url = format!("https://{}{}", req.connection_info().host(), req.uri().to_string());
|
|
||||||
srv.call(req).map(move |res: Result<ServiceResponse, _>| {
|
|
||||||
if force_https && !secure {
|
|
||||||
Ok(ServiceResponse::new(
|
|
||||||
res.unwrap().request().to_owned(),
|
|
||||||
HttpResponse::Found().header("Location", https_url).finish(),
|
|
||||||
))
|
|
||||||
} else {
|
|
||||||
res
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
// Append trailing slash and remove double slashes
|
// Append trailing slash and remove double slashes
|
||||||
.wrap(middleware::NormalizePath::default())
|
app.with(NormalizePath);
|
||||||
|
|
||||||
// Apply default headers for security
|
// Apply default headers for security
|
||||||
.wrap(
|
app.with(After(|mut res: Response| async move {
|
||||||
middleware::DefaultHeaders::new()
|
res.insert_header("Referrer-Policy", "no-referrer");
|
||||||
.header("Referrer-Policy", "no-referrer")
|
res.insert_header("X-Content-Type-Options", "nosniff");
|
||||||
.header("X-Content-Type-Options", "nosniff")
|
res.insert_header("X-Frame-Options", "DENY");
|
||||||
.header("X-Frame-Options", "DENY")
|
res.insert_header(
|
||||||
.header(
|
|
||||||
"Content-Security-Policy",
|
"Content-Security-Policy",
|
||||||
"default-src 'none'; manifest-src 'self'; media-src 'self'; style-src 'self' 'unsafe-inline'; base-uri 'none'; img-src 'self' data:; form-action 'self'; frame-ancestors 'none';",
|
"default-src 'none'; manifest-src 'self'; media-src 'self'; style-src 'self' 'unsafe-inline'; base-uri 'none'; img-src 'self' data:; form-action 'self'; frame-ancestors 'none';",
|
||||||
),
|
);
|
||||||
)
|
Ok(res)
|
||||||
// Default service in case no routes match
|
}));
|
||||||
.default_service(web::get().to(|| utils::error("Nothing here".to_string())))
|
|
||||||
// Read static files
|
// Read static files
|
||||||
.route("/style.css/", web::get().to(style))
|
app.at("/style.css/").get(style);
|
||||||
.route("/favicon.ico/", web::get().to(favicon))
|
app.at("/favicon.ico/").get(favicon);
|
||||||
.route("/robots.txt/", web::get().to(robots))
|
app.at("/robots.txt/").get(robots);
|
||||||
.route("/manifest.json/", web::get().to(manifest))
|
app.at("/manifest.json/").get(manifest);
|
||||||
.route("/logo.png/", web::get().to(pwa_logo))
|
app.at("/logo.png/").get(pwa_logo);
|
||||||
.route("/touch-icon-iphone.png/", web::get().to(iphone_logo))
|
app.at("/touch-icon-iphone.png/").get(iphone_logo);
|
||||||
|
|
||||||
// Proxy media through Libreddit
|
// Proxy media through Libreddit
|
||||||
.route("/proxy/{url:.*}/", web::get().to(proxy::handler))
|
app.at("/proxy/*url/").get(proxy::handler);
|
||||||
|
|
||||||
// Browse user profile
|
// Browse user profile
|
||||||
.service(
|
app.at("/u/:name/").get(user::profile);
|
||||||
web::scope("/{scope:user|u}").service(
|
app.at("/u/:name/comments/:id/:title/").get(post::item);
|
||||||
web::scope("/{username}").route("/", web::get().to(user::profile)).service(
|
app.at("/u/:name/comments/:id/:title/:comment/").get(post::item);
|
||||||
web::scope("/comments/{id}/{title}")
|
|
||||||
.route("/", web::get().to(post::item))
|
app.at("/user/:name/").get(user::profile);
|
||||||
.route("/{comment_id}/", web::get().to(post::item)),
|
app.at("/user/:name/comments/:id/:title/").get(post::item);
|
||||||
),
|
app.at("/user/:name/comments/:id/:title/:comment/").get(post::item);
|
||||||
),
|
|
||||||
)
|
|
||||||
// Configure settings
|
// Configure settings
|
||||||
.service(web::resource("/settings/").route(web::get().to(settings::get)).route(web::post().to(settings::set)))
|
app.at("/settings/").get(settings::get).post(settings::set);
|
||||||
|
|
||||||
// Subreddit services
|
// Subreddit services
|
||||||
.service(
|
|
||||||
web::scope("/r/{sub}")
|
|
||||||
// See posts and info about subreddit
|
// See posts and info about subreddit
|
||||||
.route("/", web::get().to(subreddit::page))
|
app.at("/r/:sub/").get(subreddit::page);
|
||||||
.route("/{sort:hot|new|top|rising|controversial}/", web::get().to(subreddit::page))
|
|
||||||
// Handle subscribe/unsubscribe
|
// Handle subscribe/unsubscribe
|
||||||
.route("/{action:subscribe|unsubscribe}/", web::post().to(subreddit::subscriptions))
|
app.at("/r/:sub/subscribe/").post(subreddit::subscriptions);
|
||||||
|
app.at("/r/:sub/unsubscribe/").post(subreddit::subscriptions);
|
||||||
// View post on subreddit
|
// View post on subreddit
|
||||||
.service(
|
app.at("/r/:sub/comments/:id/:title/").get(post::item);
|
||||||
web::scope("/comments/{id}/{title}")
|
app.at("/r/:sub/comments/:id/:title/:comment_id/").get(post::item);
|
||||||
.route("/", web::get().to(post::item))
|
|
||||||
.route("/{comment_id}/", web::get().to(post::item)),
|
|
||||||
)
|
|
||||||
// Search inside subreddit
|
// Search inside subreddit
|
||||||
.route("/search/", web::get().to(search::find))
|
app.at("/r/:sub/search/").get(search::find);
|
||||||
// View wiki of subreddit
|
// View wiki of subreddit
|
||||||
.service(
|
app.at("/r/:sub/w/").get(subreddit::wiki);
|
||||||
web::scope("/{scope:wiki|w}")
|
app.at("/r/:sub/w/:page/").get(subreddit::wiki);
|
||||||
.route("/", web::get().to(subreddit::wiki))
|
app.at("/r/:sub/wiki/").get(subreddit::wiki);
|
||||||
.route("/{page}/", web::get().to(subreddit::wiki)),
|
app.at("/r/:sub/wiki/:page/").get(subreddit::wiki);
|
||||||
),
|
// Sort subreddit posts
|
||||||
)
|
app.at("/r/:sub/:sort/").get(subreddit::page);
|
||||||
|
|
||||||
// Front page
|
// Front page
|
||||||
.route("/", web::get().to(subreddit::page))
|
app.at("/").get(subreddit::page);
|
||||||
.route("/{sort:best|hot|new|top|rising|controversial}/", web::get().to(subreddit::page))
|
|
||||||
// View Reddit wiki
|
// View Reddit wiki
|
||||||
.service(
|
app.at("/w/").get(subreddit::wiki);
|
||||||
web::scope("/wiki")
|
app.at("/w/:page/").get(subreddit::wiki);
|
||||||
.route("/", web::get().to(subreddit::wiki))
|
app.at("/wiki/").get(subreddit::wiki);
|
||||||
.route("/{page}/", web::get().to(subreddit::wiki)),
|
app.at("/wiki/:page/").get(subreddit::wiki);
|
||||||
)
|
|
||||||
// Search all of Reddit
|
// Search all of Reddit
|
||||||
.route("/search/", web::get().to(search::find))
|
app.at("/search/").get(search::find);
|
||||||
|
|
||||||
// Short link for post
|
// Short link for post
|
||||||
.route("/{id:.{5,6}}/", web::get().to(post::item))
|
// .route("/{sort:best|hot|new|top|rising|controversial}/", web::get().to(subreddit::page))
|
||||||
})
|
// .route("/{id:.{5,6}}/", web::get().to(post::item))
|
||||||
.bind(&address)
|
app.at("/:id/").get(|req: Request<()>| async {
|
||||||
.unwrap_or_else(|e| panic!("Cannot bind to the address {}: {}", address, e))
|
match req.param("id").unwrap_or_default() {
|
||||||
.run()
|
"best" | "hot" | "new" | "top" | "rising" | "controversial" => subreddit::page(req).await,
|
||||||
.await
|
_ => post::item(req).await,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Default service in case no routes match
|
||||||
|
app.at("*").get(|_| utils::error("Nothing here".to_string()));
|
||||||
|
|
||||||
|
app.listen("127.0.0.1:8080").await?;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
22
src/post.rs
22
src/post.rs
@ -1,6 +1,6 @@
|
|||||||
// CRATES
|
// CRATES
|
||||||
use crate::utils::*;
|
use crate::utils::*;
|
||||||
use actix_web::{HttpRequest, HttpResponse};
|
use tide::Request;
|
||||||
|
|
||||||
use async_recursion::async_recursion;
|
use async_recursion::async_recursion;
|
||||||
|
|
||||||
@ -16,9 +16,9 @@ struct PostTemplate {
|
|||||||
prefs: Preferences,
|
prefs: Preferences,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn item(req: HttpRequest) -> HttpResponse {
|
pub async fn item(req: Request<()>) -> tide::Result {
|
||||||
// Build Reddit API path
|
// Build Reddit API path
|
||||||
let mut path: String = format!("{}.json?{}&raw_json=1", req.path(), req.query_string());
|
let mut path: String = format!("{}.json?{}&raw_json=1", req.url().path(), req.url().query().unwrap_or_default());
|
||||||
|
|
||||||
// Set sort to sort query parameter
|
// Set sort to sort query parameter
|
||||||
let mut sort: String = param(&path, "sort");
|
let mut sort: String = param(&path, "sort");
|
||||||
@ -29,12 +29,17 @@ pub async fn item(req: HttpRequest) -> HttpResponse {
|
|||||||
// If there's no sort query but there's a default sort, set sort to default_sort
|
// If there's no sort query but there's a default sort, set sort to default_sort
|
||||||
if sort.is_empty() && !default_sort.is_empty() {
|
if sort.is_empty() && !default_sort.is_empty() {
|
||||||
sort = default_sort;
|
sort = default_sort;
|
||||||
path = format!("{}.json?{}&sort={}&raw_json=1", req.path(), req.query_string(), sort);
|
path = format!(
|
||||||
|
"{}.json?{}&sort={}&raw_json=1",
|
||||||
|
req.url().path(),
|
||||||
|
req.url().query().unwrap_or_default(),
|
||||||
|
sort
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log the post ID being fetched in debug mode
|
// Log the post ID being fetched in debug mode
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
dbg!(req.match_info().get("id").unwrap_or(""));
|
dbg!(req.param("id").unwrap_or(""));
|
||||||
|
|
||||||
// Send a request to the url, receive JSON in response
|
// Send a request to the url, receive JSON in response
|
||||||
match request(path).await {
|
match request(path).await {
|
||||||
@ -45,15 +50,12 @@ pub async fn item(req: HttpRequest) -> HttpResponse {
|
|||||||
let comments = parse_comments(&res[1]).await;
|
let comments = parse_comments(&res[1]).await;
|
||||||
|
|
||||||
// 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
|
||||||
let s = PostTemplate {
|
template(PostTemplate {
|
||||||
comments,
|
comments,
|
||||||
post,
|
post,
|
||||||
sort,
|
sort,
|
||||||
prefs: prefs(req),
|
prefs: prefs(req),
|
||||||
}
|
})
|
||||||
.render()
|
|
||||||
.unwrap();
|
|
||||||
HttpResponse::Ok().content_type("text/html").body(s)
|
|
||||||
}
|
}
|
||||||
// If the Reddit API returns an error, exit and send error page to user
|
// If the Reddit API returns an error, exit and send error page to user
|
||||||
Err(msg) => error(msg).await,
|
Err(msg) => error(msg).await,
|
||||||
|
35
src/proxy.rs
35
src/proxy.rs
@ -1,9 +1,8 @@
|
|||||||
use actix_web::{client::Client, error, web, Error, HttpResponse, Result};
|
|
||||||
use url::Url;
|
|
||||||
|
|
||||||
use base64::decode;
|
use base64::decode;
|
||||||
|
use surf::{Body, Url};
|
||||||
|
use tide::{Request, Response};
|
||||||
|
|
||||||
pub async fn handler(web::Path(b64): web::Path<String>) -> Result<HttpResponse> {
|
pub async fn handler(req: Request<()>) -> tide::Result {
|
||||||
let domains = vec![
|
let domains = vec![
|
||||||
// THUMBNAILS
|
// THUMBNAILS
|
||||||
"a.thumbs.redditmedia.com",
|
"a.thumbs.redditmedia.com",
|
||||||
@ -21,27 +20,31 @@ pub async fn handler(web::Path(b64): web::Path<String>) -> Result<HttpResponse>
|
|||||||
"v.redd.it",
|
"v.redd.it",
|
||||||
];
|
];
|
||||||
|
|
||||||
let decoded = decode(b64).map(|bytes| String::from_utf8(bytes).unwrap_or_default());
|
let decoded = decode(req.param("url").unwrap_or_default()).map(|bytes| String::from_utf8(bytes).unwrap_or_default());
|
||||||
|
|
||||||
match decoded {
|
match decoded {
|
||||||
Ok(media) => match Url::parse(media.as_str()) {
|
Ok(media) => match Url::parse(media.as_str()) {
|
||||||
Ok(url) => {
|
Ok(url) => {
|
||||||
let domain = url.domain().unwrap_or_default();
|
if domains.contains(&url.domain().unwrap_or_default()) {
|
||||||
|
let http = surf::get(url).await.unwrap();
|
||||||
|
|
||||||
if domains.contains(&domain) {
|
let content_length = http.header("Content-Length").map(|v| v.to_string()).unwrap_or_default();
|
||||||
Client::default().get(media.replace("&", "&")).send().await.map_err(Error::from).map(|res| {
|
let content_type = http.content_type().map(|m| m.to_string()).unwrap_or_default();
|
||||||
HttpResponse::build(res.status())
|
|
||||||
|
Ok(
|
||||||
|
Response::builder(http.status())
|
||||||
|
.body(Body::from_reader(http, None))
|
||||||
.header("Cache-Control", "public, max-age=1209600, s-maxage=86400")
|
.header("Cache-Control", "public, max-age=1209600, s-maxage=86400")
|
||||||
.header("Content-Length", res.headers().get("Content-Length").unwrap().to_owned())
|
.header("Content-Length", content_length)
|
||||||
.header("Content-Type", res.headers().get("Content-Type").unwrap().to_owned())
|
.header("Content-Type", content_type)
|
||||||
.streaming(res)
|
.build(),
|
||||||
})
|
)
|
||||||
} else {
|
} else {
|
||||||
Err(error::ErrorForbidden("Resource must be from Reddit"))
|
Err(tide::Error::from_str(403, "Resource must be from Reddit"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => Err(error::ErrorBadRequest("Can't parse base64 into URL")),
|
Err(_) => Err(tide::Error::from_str(400, "Can't parse base64 into URL")),
|
||||||
},
|
},
|
||||||
_ => Err(error::ErrorBadRequest("Can't decode base64")),
|
Err(_) => Err(tide::Error::from_str(400, "Can't decode base64")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
// CRATES
|
// CRATES
|
||||||
use crate::utils::{cookie, error, fetch_posts, param, prefs, request, val, Post, Preferences};
|
use crate::utils::{cookie, error, fetch_posts, param, prefs, request, template, val, Post, Preferences};
|
||||||
use actix_web::{HttpRequest, HttpResponse};
|
|
||||||
use askama::Template;
|
use askama::Template;
|
||||||
|
use tide::Request;
|
||||||
|
|
||||||
// STRUCTS
|
// STRUCTS
|
||||||
struct SearchParams {
|
struct SearchParams {
|
||||||
@ -32,10 +32,10 @@ struct SearchTemplate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SERVICES
|
// SERVICES
|
||||||
pub async fn find(req: HttpRequest) -> HttpResponse {
|
pub async fn find(req: Request<()>) -> tide::Result {
|
||||||
let nsfw_results = if cookie(&req, "show_nsfw") == "on" { "&include_over_18=on" } else { "" };
|
let nsfw_results = if cookie(&req, "show_nsfw") == "on" { "&include_over_18=on" } else { "" };
|
||||||
let path = format!("{}.json?{}{}", req.path(), req.query_string(), nsfw_results);
|
let path = format!("{}.json?{}{}", req.url().path(), req.url().query().unwrap_or_default(), nsfw_results);
|
||||||
let sub = req.match_info().get("sub").unwrap_or("").to_string();
|
let sub = req.param("sub").unwrap_or("").to_string();
|
||||||
|
|
||||||
let sort = if param(&path, "sort").is_empty() {
|
let sort = if param(&path, "sort").is_empty() {
|
||||||
"relevance".to_string()
|
"relevance".to_string()
|
||||||
@ -50,8 +50,7 @@ pub async fn find(req: HttpRequest) -> HttpResponse {
|
|||||||
};
|
};
|
||||||
|
|
||||||
match fetch_posts(&path, String::new()).await {
|
match fetch_posts(&path, String::new()).await {
|
||||||
Ok((posts, after)) => HttpResponse::Ok().content_type("text/html").body(
|
Ok((posts, after)) => template(SearchTemplate {
|
||||||
SearchTemplate {
|
|
||||||
posts,
|
posts,
|
||||||
subreddits,
|
subreddits,
|
||||||
sub,
|
sub,
|
||||||
@ -64,10 +63,7 @@ pub async fn find(req: HttpRequest) -> HttpResponse {
|
|||||||
restrict_sr: param(&path, "restrict_sr"),
|
restrict_sr: param(&path, "restrict_sr"),
|
||||||
},
|
},
|
||||||
prefs: prefs(req),
|
prefs: prefs(req),
|
||||||
}
|
}),
|
||||||
.render()
|
|
||||||
.unwrap(),
|
|
||||||
),
|
|
||||||
Err(msg) => error(msg).await,
|
Err(msg) => error(msg).await,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
// CRATES
|
// CRATES
|
||||||
use crate::utils::{prefs, Preferences};
|
use crate::utils::{prefs, template, Preferences};
|
||||||
use actix_web::{cookie::Cookie, web::Form, HttpRequest, HttpResponse};
|
|
||||||
use askama::Template;
|
use askama::Template;
|
||||||
|
use tide::{http::Cookie, Request, Response};
|
||||||
use time::{Duration, OffsetDateTime};
|
use time::{Duration, OffsetDateTime};
|
||||||
|
|
||||||
// STRUCTS
|
// STRUCTS
|
||||||
@ -11,7 +11,7 @@ struct SettingsTemplate {
|
|||||||
prefs: Preferences,
|
prefs: Preferences,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Deserialize)]
|
#[derive(serde::Deserialize, Default)]
|
||||||
pub struct SettingsForm {
|
pub struct SettingsForm {
|
||||||
theme: Option<String>,
|
theme: Option<String>,
|
||||||
front_page: Option<String>,
|
front_page: Option<String>,
|
||||||
@ -24,33 +24,35 @@ pub struct SettingsForm {
|
|||||||
// FUNCTIONS
|
// FUNCTIONS
|
||||||
|
|
||||||
// Retrieve cookies from request "Cookie" header
|
// Retrieve cookies from request "Cookie" header
|
||||||
pub async fn get(req: HttpRequest) -> HttpResponse {
|
pub async fn get(req: Request<()>) -> tide::Result {
|
||||||
let s = SettingsTemplate { prefs: prefs(req) }.render().unwrap();
|
template(SettingsTemplate { prefs: prefs(req) })
|
||||||
HttpResponse::Ok().content_type("text/html").body(s)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set cookies using response "Set-Cookie" header
|
// Set cookies using response "Set-Cookie" header
|
||||||
pub async fn set(_req: HttpRequest, form: Form<SettingsForm>) -> HttpResponse {
|
pub async fn set(mut req: Request<()>) -> tide::Result {
|
||||||
let mut res = HttpResponse::Found();
|
let form: SettingsForm = req.body_form().await.unwrap_or_default();
|
||||||
|
|
||||||
|
let mut res = Response::builder(302)
|
||||||
|
.content_type("text/html")
|
||||||
|
.header("Location", "/settings")
|
||||||
|
.body(r#"Redirecting to <a href="/settings">settings</a>..."#)
|
||||||
|
.build();
|
||||||
|
|
||||||
let names = vec!["theme", "front_page", "layout", "wide", "comment_sort", "show_nsfw"];
|
let names = vec!["theme", "front_page", "layout", "wide", "comment_sort", "show_nsfw"];
|
||||||
let values = vec![&form.theme, &form.front_page, &form.layout, &form.wide, &form.comment_sort, &form.show_nsfw];
|
let values = vec![form.theme, form.front_page, form.layout, form.wide, form.comment_sort, form.show_nsfw];
|
||||||
|
|
||||||
for (i, name) in names.iter().enumerate() {
|
for (i, name) in names.iter().enumerate() {
|
||||||
match values[i] {
|
match values.get(i) {
|
||||||
Some(value) => res.cookie(
|
Some(value) => res.insert_cookie(
|
||||||
Cookie::build(name.to_owned(), value)
|
Cookie::build(name.to_owned(), value.to_owned().unwrap_or_default())
|
||||||
.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.del_cookie(&Cookie::named(name.to_owned())),
|
None => res.remove_cookie(Cookie::named(name.to_owned())),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
res
|
Ok(res)
|
||||||
.content_type("text/html")
|
|
||||||
.set_header("Location", "/settings")
|
|
||||||
.body(r#"Redirecting to <a href="/settings">settings</a>..."#)
|
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
// CRATES
|
// CRATES
|
||||||
use crate::utils::*;
|
use crate::utils::*;
|
||||||
use actix_web::{cookie::Cookie, HttpRequest, HttpResponse, Result};
|
|
||||||
use askama::Template;
|
use askama::Template;
|
||||||
|
use tide::{http::Cookie, Request, Response};
|
||||||
use time::{Duration, OffsetDateTime};
|
use time::{Duration, OffsetDateTime};
|
||||||
|
|
||||||
// STRUCTS
|
// STRUCTS
|
||||||
@ -25,14 +25,14 @@ struct WikiTemplate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SERVICES
|
// SERVICES
|
||||||
pub async fn page(req: HttpRequest) -> HttpResponse {
|
pub async fn page(req: Request<()>) -> tide::Result {
|
||||||
|
// Build Reddit API path
|
||||||
let subscribed = cookie(&req, "subscriptions");
|
let subscribed = cookie(&req, "subscriptions");
|
||||||
let front_page = cookie(&req, "front_page");
|
let front_page = cookie(&req, "front_page");
|
||||||
let sort = req.match_info().get("sort").unwrap_or("hot").to_string();
|
let sort = req.param("sort").unwrap_or_else(|_| req.param("id").unwrap_or("hot")).to_string();
|
||||||
|
|
||||||
let sub = req
|
let sub = req
|
||||||
.match_info()
|
.param("sub")
|
||||||
.get("sub")
|
|
||||||
.map(String::from)
|
.map(String::from)
|
||||||
.unwrap_or(if front_page == "default" || front_page.is_empty() {
|
.unwrap_or(if front_page == "default" || front_page.is_empty() {
|
||||||
if subscribed.is_empty() {
|
if subscribed.is_empty() {
|
||||||
@ -44,7 +44,7 @@ pub async fn page(req: HttpRequest) -> HttpResponse {
|
|||||||
front_page.to_owned()
|
front_page.to_owned()
|
||||||
});
|
});
|
||||||
|
|
||||||
let path = format!("/r/{}/{}.json?{}", sub, sort, req.query_string());
|
let path = format!("/r/{}/{}.json?{}&raw_json=1", sub, sort, req.url().query().unwrap_or_default());
|
||||||
|
|
||||||
match fetch_posts(&path, String::new()).await {
|
match fetch_posts(&path, String::new()).await {
|
||||||
Ok((posts, after)) => {
|
Ok((posts, after)) => {
|
||||||
@ -54,7 +54,7 @@ pub async fn page(req: HttpRequest) -> HttpResponse {
|
|||||||
subreddit(&sub).await.unwrap_or_default()
|
subreddit(&sub).await.unwrap_or_default()
|
||||||
} else if sub == subscribed {
|
} else if sub == subscribed {
|
||||||
// Subscription feed
|
// Subscription feed
|
||||||
if req.path().starts_with("/r/") {
|
if req.url().path().starts_with("/r/") {
|
||||||
subreddit(&sub).await.unwrap_or_default()
|
subreddit(&sub).await.unwrap_or_default()
|
||||||
} else {
|
} else {
|
||||||
Subreddit::default()
|
Subreddit::default()
|
||||||
@ -69,42 +69,55 @@ pub async fn page(req: HttpRequest) -> HttpResponse {
|
|||||||
Subreddit::default()
|
Subreddit::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
let s = SubredditTemplate {
|
template(SubredditTemplate {
|
||||||
sub,
|
sub,
|
||||||
posts,
|
posts,
|
||||||
sort: (sort, param(&path, "t")),
|
sort: (sort, param(&path, "t")),
|
||||||
ends: (param(&path, "after"), after),
|
ends: (param(&path, "after"), after),
|
||||||
prefs: prefs(req),
|
prefs: prefs(req),
|
||||||
}
|
})
|
||||||
.render()
|
|
||||||
.unwrap();
|
|
||||||
HttpResponse::Ok().content_type("text/html").body(s)
|
|
||||||
}
|
}
|
||||||
Err(msg) => error(msg).await,
|
Err(msg) => error(msg).await,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sub or unsub by setting subscription cookie using response "Set-Cookie" header
|
// Sub or unsub by setting subscription cookie using response "Set-Cookie" header
|
||||||
pub async fn subscriptions(req: HttpRequest) -> HttpResponse {
|
pub async fn subscriptions(req: Request<()>) -> tide::Result {
|
||||||
let mut res = HttpResponse::Found();
|
let sub = req.param("sub").unwrap_or_default().to_string();
|
||||||
|
let query = req.url().query().unwrap_or_default().to_string();
|
||||||
|
let action: Vec<String> = req.url().path().split('/').map(String::from).collect();
|
||||||
|
|
||||||
let sub = req.match_info().get("sub").unwrap_or_default().to_string();
|
let mut sub_list = prefs(req).subs;
|
||||||
let action = req.match_info().get("action").unwrap_or_default().to_string();
|
|
||||||
let mut sub_list = prefs(req.to_owned()).subs;
|
|
||||||
|
|
||||||
// Modify sub list based on action
|
// Modify sub list based on action
|
||||||
if action == "subscribe" && !sub_list.contains(&sub) {
|
if action.contains(&"subscribe".to_string()) && !sub_list.contains(&sub) {
|
||||||
sub_list.push(sub.to_owned());
|
sub_list.push(sub.to_owned());
|
||||||
sub_list.sort_by_key(|a| a.to_lowercase());
|
sub_list.sort_by_key(|a| a.to_lowercase())
|
||||||
} else if action == "unsubscribe" {
|
} else if action.contains(&"unsubscribe".to_string()) {
|
||||||
sub_list.retain(|s| s != &sub);
|
sub_list.retain(|s| s != &sub);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Redirect back to subreddit
|
||||||
|
// check for redirect parameter if unsubscribing from outside sidebar
|
||||||
|
let redirect_path = param(format!("/?{}", query).as_str(), "redirect");
|
||||||
|
let path = if !redirect_path.is_empty() {
|
||||||
|
format!("/{}/", redirect_path)
|
||||||
|
} else {
|
||||||
|
format!("/r/{}", sub)
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut res = Response::builder(302)
|
||||||
|
.content_type("text/html")
|
||||||
|
.header("Location", path.to_owned())
|
||||||
|
.body(format!("Redirecting to <a href=\"{0}\">{0}</a>...", path))
|
||||||
|
.build();
|
||||||
|
|
||||||
// Delete cookie if empty, else set
|
// Delete cookie if empty, else set
|
||||||
if sub_list.is_empty() {
|
if sub_list.is_empty() {
|
||||||
res.del_cookie(&Cookie::build("subscriptions", "").path("/").finish());
|
// res.del_cookie(&Cookie::build("subscriptions", "").path("/").finish());
|
||||||
|
res.remove_cookie(Cookie::build("subscriptions", "").path("/").finish());
|
||||||
} else {
|
} else {
|
||||||
res.cookie(
|
res.insert_cookie(
|
||||||
Cookie::build("subscriptions", sub_list.join("+"))
|
Cookie::build("subscriptions", sub_list.join("+"))
|
||||||
.path("/")
|
.path("/")
|
||||||
.http_only(true)
|
.http_only(true)
|
||||||
@ -113,38 +126,21 @@ pub async fn subscriptions(req: HttpRequest) -> HttpResponse {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Redirect back to subreddit
|
Ok(res)
|
||||||
// check for redirect parameter if unsubscribing from outside sidebar
|
|
||||||
let redirect_path = param(&req.uri().to_string(), "redirect");
|
|
||||||
let path = if !redirect_path.is_empty() && redirect_path.starts_with('/') {
|
|
||||||
redirect_path
|
|
||||||
} else {
|
|
||||||
format!("/r/{}", sub)
|
|
||||||
};
|
|
||||||
|
|
||||||
res
|
|
||||||
.content_type("text/html")
|
|
||||||
.set_header("Location", path.to_owned())
|
|
||||||
.body(format!("Redirecting to <a href=\"{0}\">{0}</a>...", path))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn wiki(req: HttpRequest) -> HttpResponse {
|
pub async fn wiki(req: Request<()>) -> tide::Result {
|
||||||
let sub = req.match_info().get("sub").unwrap_or("reddit.com").to_string();
|
let sub = req.param("sub").unwrap_or("reddit.com").to_string();
|
||||||
let page = req.match_info().get("page").unwrap_or("index").to_string();
|
let page = req.param("page").unwrap_or("index").to_string();
|
||||||
let path: String = format!("/r/{}/wiki/{}.json?raw_json=1", sub, page);
|
let path: String = format!("/r/{}/wiki/{}.json?raw_json=1", sub, page);
|
||||||
|
|
||||||
match request(path).await {
|
match request(path).await {
|
||||||
Ok(res) => {
|
Ok(res) => template(WikiTemplate {
|
||||||
let s = WikiTemplate {
|
|
||||||
sub,
|
sub,
|
||||||
wiki: rewrite_url(res["data"]["content_html"].as_str().unwrap_or_default()),
|
wiki: rewrite_url(res["data"]["content_html"].as_str().unwrap_or_default()),
|
||||||
page,
|
page,
|
||||||
prefs: prefs(req),
|
prefs: prefs(req),
|
||||||
}
|
}),
|
||||||
.render()
|
|
||||||
.unwrap();
|
|
||||||
HttpResponse::Ok().content_type("text/html").body(s)
|
|
||||||
}
|
|
||||||
Err(msg) => error(msg).await,
|
Err(msg) => error(msg).await,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -163,8 +159,14 @@ async fn subreddit(sub: &str) -> Result<Subreddit, String> {
|
|||||||
let active: i64 = res["data"]["accounts_active"].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
|
// Fetch subreddit icon either from the community_icon or icon_img value
|
||||||
let community_icon: &str = res["data"]["community_icon"].as_str().map_or("", |s| s.split('?').collect::<Vec<&str>>()[0]);
|
let community_icon: &str = res["data"]["community_icon"]
|
||||||
let icon = if community_icon.is_empty() { val(&res, "icon_img") } else { community_icon.to_string() };
|
.as_str()
|
||||||
|
.map_or("", |s| s.split('?').collect::<Vec<&str>>()[0]);
|
||||||
|
let icon = if community_icon.is_empty() {
|
||||||
|
val(&res, "icon_img")
|
||||||
|
} else {
|
||||||
|
community_icon.to_string()
|
||||||
|
};
|
||||||
|
|
||||||
let sub = Subreddit {
|
let sub = Subreddit {
|
||||||
name: val(&res, "display_name"),
|
name: val(&res, "display_name"),
|
||||||
|
19
src/user.rs
19
src/user.rs
@ -1,7 +1,7 @@
|
|||||||
// CRATES
|
// CRATES
|
||||||
use crate::utils::{error, fetch_posts, format_url, param, prefs, request, Post, Preferences, User};
|
use crate::utils::*;
|
||||||
use actix_web::{HttpRequest, HttpResponse, Result};
|
|
||||||
use askama::Template;
|
use askama::Template;
|
||||||
|
use tide::Request;
|
||||||
use time::OffsetDateTime;
|
use time::OffsetDateTime;
|
||||||
|
|
||||||
// STRUCTS
|
// STRUCTS
|
||||||
@ -16,13 +16,13 @@ struct UserTemplate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// FUNCTIONS
|
// FUNCTIONS
|
||||||
pub async fn profile(req: HttpRequest) -> HttpResponse {
|
pub async fn profile(req: Request<()>) -> tide::Result {
|
||||||
// Build the Reddit JSON API path
|
// Build the Reddit JSON API path
|
||||||
let path = format!("{}.json?{}&raw_json=1", req.path(), req.query_string());
|
let path = format!("{}.json?{}&raw_json=1", req.url().path(), req.url().query().unwrap_or_default());
|
||||||
|
|
||||||
// Retrieve other variables from Libreddit request
|
// Retrieve other variables from Libreddit request
|
||||||
let sort = param(&path, "sort");
|
let sort = param(&path, "sort");
|
||||||
let username = req.match_info().get("username").unwrap_or("").to_string();
|
let username = req.param("name").unwrap_or("").to_string();
|
||||||
|
|
||||||
// Request user posts/comments from Reddit
|
// Request user posts/comments from Reddit
|
||||||
let posts = fetch_posts(&path, "Comment".to_string()).await;
|
let posts = fetch_posts(&path, "Comment".to_string()).await;
|
||||||
@ -32,16 +32,13 @@ pub async fn profile(req: HttpRequest) -> HttpResponse {
|
|||||||
// If you can get user posts, also request user data
|
// If you can get user posts, also request user data
|
||||||
let user = user(&username).await.unwrap_or_default();
|
let user = user(&username).await.unwrap_or_default();
|
||||||
|
|
||||||
let s = UserTemplate {
|
template(UserTemplate {
|
||||||
user,
|
user,
|
||||||
posts,
|
posts,
|
||||||
sort: (sort, param(&path, "t")),
|
sort: (sort, param(&path, "t")),
|
||||||
ends: (param(&path, "after"), after),
|
ends: (param(&path, "after"), after),
|
||||||
prefs: prefs(req),
|
prefs: prefs(req),
|
||||||
}
|
})
|
||||||
.render()
|
|
||||||
.unwrap();
|
|
||||||
HttpResponse::Ok().content_type("text/html").body(s)
|
|
||||||
}
|
}
|
||||||
// If there is an error show error page
|
// If there is an error show error page
|
||||||
Err(msg) => error(msg).await,
|
Err(msg) => error(msg).await,
|
||||||
@ -51,7 +48,7 @@ pub async fn profile(req: HttpRequest) -> HttpResponse {
|
|||||||
// USER
|
// USER
|
||||||
async fn user(name: &str) -> Result<User, String> {
|
async fn user(name: &str) -> Result<User, String> {
|
||||||
// Build the Reddit JSON API path
|
// Build the Reddit JSON API path
|
||||||
let path: String = format!("/user/{}/about.json", 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 request(path).await {
|
match request(path).await {
|
||||||
|
115
src/utils.rs
115
src/utils.rs
@ -1,15 +1,14 @@
|
|||||||
//
|
//
|
||||||
// CRATES
|
// CRATES
|
||||||
//
|
//
|
||||||
use actix_web::{cookie::Cookie, HttpRequest, HttpResponse, Result};
|
|
||||||
use askama::Template;
|
use askama::Template;
|
||||||
use base64::encode;
|
use base64::encode;
|
||||||
use cached::proc_macro::cached;
|
use cached::proc_macro::cached;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use serde_json::{from_str, Value};
|
use serde_json::{from_str, Value};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use tide::{http::url::Url, http::Cookie, Request, Response};
|
||||||
use time::{Duration, OffsetDateTime};
|
use time::{Duration, OffsetDateTime};
|
||||||
use url::Url;
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// STRUCTS
|
// STRUCTS
|
||||||
@ -147,7 +146,7 @@ pub struct Preferences {
|
|||||||
//
|
//
|
||||||
|
|
||||||
// Build preferences from cookies
|
// Build preferences from cookies
|
||||||
pub fn prefs(req: HttpRequest) -> Preferences {
|
pub fn prefs(req: Request<()>) -> Preferences {
|
||||||
Preferences {
|
Preferences {
|
||||||
theme: cookie(&req, "theme"),
|
theme: cookie(&req, "theme"),
|
||||||
front_page: cookie(&req, "front_page"),
|
front_page: cookie(&req, "front_page"),
|
||||||
@ -155,21 +154,32 @@ pub fn prefs(req: HttpRequest) -> Preferences {
|
|||||||
wide: cookie(&req, "wide"),
|
wide: cookie(&req, "wide"),
|
||||||
show_nsfw: cookie(&req, "show_nsfw"),
|
show_nsfw: cookie(&req, "show_nsfw"),
|
||||||
comment_sort: cookie(&req, "comment_sort"),
|
comment_sort: cookie(&req, "comment_sort"),
|
||||||
subs: cookie(&req, "subscriptions").split('+').map(String::from).filter(|s| !s.is_empty()).collect(),
|
subs: cookie(&req, "subscriptions")
|
||||||
|
.split('+')
|
||||||
|
.map(String::from)
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
.collect(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Grab a query param from a url
|
// Grab a query param from a url
|
||||||
pub fn param(path: &str, value: &str) -> String {
|
pub fn param(path: &str, value: &str) -> String {
|
||||||
match Url::parse(format!("https://libredd.it/{}", path).as_str()) {
|
match Url::parse(format!("https://libredd.it/{}", path).as_str()) {
|
||||||
Ok(url) => url.query_pairs().into_owned().collect::<HashMap<_, _>>().get(value).unwrap_or(&String::new()).to_owned(),
|
Ok(url) => url
|
||||||
|
.query_pairs()
|
||||||
|
.into_owned()
|
||||||
|
.collect::<HashMap<_, _>>()
|
||||||
|
.get(value)
|
||||||
|
.unwrap_or(&String::new())
|
||||||
|
.to_owned(),
|
||||||
_ => String::new(),
|
_ => String::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse Cookie value from request
|
// Parse Cookie value from request
|
||||||
pub fn cookie(req: &HttpRequest, name: &str) -> String {
|
pub fn cookie(req: &Request<()>, name: &str) -> String {
|
||||||
actix_web::HttpMessage::cookie(req, name).unwrap_or_else(|| Cookie::new(name, "")).value().to_string()
|
let cookie = req.cookie(name).unwrap_or_else(|| Cookie::named(name));
|
||||||
|
cookie.value().to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Direct urls to proxy if proxy is enabled
|
// Direct urls to proxy if proxy is enabled
|
||||||
@ -177,7 +187,7 @@ 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 {
|
||||||
format!("/proxy/{}", encode(url).as_str())
|
format!("/proxy/{}/", encode(url).as_str())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -420,102 +430,57 @@ pub async fn fetch_posts(path: &str, fallback_title: String) -> Result<(Vec<Post
|
|||||||
// NETWORKING
|
// NETWORKING
|
||||||
//
|
//
|
||||||
|
|
||||||
pub async fn error(msg: String) -> HttpResponse {
|
pub fn template(f: impl Template) -> tide::Result {
|
||||||
|
Ok(
|
||||||
|
Response::builder(200)
|
||||||
|
.content_type("text/html")
|
||||||
|
.body(f.render().unwrap_or_default())
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn error(msg: String) -> tide::Result {
|
||||||
let body = ErrorTemplate {
|
let body = ErrorTemplate {
|
||||||
msg,
|
msg,
|
||||||
prefs: Preferences::default(),
|
prefs: Preferences::default(),
|
||||||
}
|
}
|
||||||
.render()
|
.render()
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
HttpResponse::NotFound().content_type("text/html").body(body)
|
|
||||||
|
Ok(Response::builder(404).content_type("text/html").body(body).build())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make a request to a Reddit API and parse the JSON response
|
// Make a request to a Reddit API and parse the JSON response
|
||||||
#[cached(size = 100, time = 30, result = true)]
|
#[cached(size = 100, time = 30, result = true)]
|
||||||
pub async fn request(path: String) -> Result<Value, String> {
|
pub async fn request(path: String) -> Result<Value, String> {
|
||||||
let url = format!("https://www.reddit.com{}", path);
|
let url = format!("https://www.reddit.com{}", path);
|
||||||
|
// Build reddit-compliant user agent for Libreddit
|
||||||
let user_agent = format!("web:libreddit:{}", env!("CARGO_PKG_VERSION"));
|
let user_agent = format!("web:libreddit:{}", env!("CARGO_PKG_VERSION"));
|
||||||
|
|
||||||
// Send request using awc
|
// Send request using surf
|
||||||
// async fn send(url: &str) -> Result<String, (bool, String)> {
|
let req = surf::get(&url).header("User-Agent", user_agent.as_str());
|
||||||
// let client = actix_web::client::Client::default();
|
let client = surf::client().with(surf::middleware::Redirect::new(5));
|
||||||
// let response = client.get(url).header("User-Agent", format!("web:libreddit:{}", env!("CARGO_PKG_VERSION"))).send().await;
|
|
||||||
|
|
||||||
// match response {
|
let res = client.send(req).await;
|
||||||
// Ok(mut payload) => {
|
|
||||||
// // Get first number of response HTTP status code
|
|
||||||
// match payload.status().to_string().chars().next() {
|
|
||||||
// // If success
|
|
||||||
// Some('2') => Ok(String::from_utf8(payload.body().limit(20_000_000).await.unwrap_or_default().to_vec()).unwrap_or_default()),
|
|
||||||
// // If redirection
|
|
||||||
// Some('3') => match payload.headers().get("location") {
|
|
||||||
// Some(location) => Err((true, location.to_str().unwrap_or_default().to_string())),
|
|
||||||
// None => Err((false, "Page not found".to_string())),
|
|
||||||
// },
|
|
||||||
// // Otherwise
|
|
||||||
// _ => Err((false, "Page not found".to_string())),
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// Err(e) => { dbg!(e); Err((false, "Couldn't send request to Reddit, this instance may be being rate-limited. Try another.".to_string())) },
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Print error if debugging then return error based on error message
|
let body = res.unwrap().take_body().into_string().await;
|
||||||
// fn err(url: String, msg: String) -> Result<Value, String> {
|
|
||||||
// // #[cfg(debug_assertions)]
|
|
||||||
// dbg!(format!("{} - {}", url, msg));
|
|
||||||
// Err(msg)
|
|
||||||
// };
|
|
||||||
|
|
||||||
// // Parse JSON from body. If parsing fails, return error
|
match body {
|
||||||
// fn json(url: String, body: String) -> Result<Value, String> {
|
|
||||||
// match from_str(body.as_str()) {
|
|
||||||
// Ok(json) => Ok(json),
|
|
||||||
// Err(_) => err(url, "Failed to parse page JSON data".to_string()),
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Make request to Reddit using send function
|
|
||||||
// match send(&url).await {
|
|
||||||
// // If success, parse and return body
|
|
||||||
// Ok(body) => json(url, body),
|
|
||||||
// // Follow any redirects
|
|
||||||
// Err((true, location)) => match send(location.as_str()).await {
|
|
||||||
// // If success, parse and return body
|
|
||||||
// Ok(body) => json(url, body),
|
|
||||||
// // Follow any redirects again
|
|
||||||
// Err((true, location)) => err(url, location),
|
|
||||||
// // Return errors if request fails
|
|
||||||
// Err((_, msg)) => err(url, msg),
|
|
||||||
// },
|
|
||||||
// // Return errors if request fails
|
|
||||||
// Err((_, msg)) => err(url, msg),
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Send request using ureq
|
|
||||||
match ureq::get(&url).set("User-Agent", user_agent.as_str()).call() {
|
|
||||||
// If response is success
|
// If response is success
|
||||||
Ok(response) => {
|
Ok(response) => {
|
||||||
// Parse the response from Reddit as JSON
|
// Parse the response from Reddit as JSON
|
||||||
let json_string = &response.into_string().unwrap_or_default();
|
match from_str(&response) {
|
||||||
match from_str(json_string) {
|
|
||||||
Ok(json) => Ok(json),
|
Ok(json) => Ok(json),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
println!("{} - Failed to parse page JSON data: {} - {}", url, e, json_string);
|
println!("{} - Failed to parse page JSON data: {}", url, e);
|
||||||
Err("Failed to parse page JSON data".to_string())
|
Err("Failed to parse page JSON data".to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// If response is error
|
|
||||||
Err(ureq::Error::Status(_, _)) => {
|
|
||||||
#[cfg(debug_assertions)]
|
|
||||||
dbg!(format!("{} - Page not found", url));
|
|
||||||
Err("Page not found".to_string())
|
|
||||||
}
|
|
||||||
// If failed to send request
|
// If failed to send request
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
println!("{} - Couldn't send request to Reddit: {}", url, e);
|
println!("{} - Couldn't send request to Reddit: {}", url, e);
|
||||||
Err("Couldn't send request to Reddit, this instance may be being rate-limited. Try another.".to_string())
|
Err("Couldn't send request to Reddit".to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,11 +15,11 @@
|
|||||||
<!-- Android -->
|
<!-- Android -->
|
||||||
<meta name="mobile-web-app-capable" content="yes">
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
<!-- iOS Logo -->
|
<!-- iOS Logo -->
|
||||||
<link href="/touch-icon-iphone.png" rel="apple-touch-icon">
|
<link href="/touch-icon-iphone.png/" rel="apple-touch-icon">
|
||||||
<!-- PWA Manifest -->
|
<!-- PWA Manifest -->
|
||||||
<link rel="manifest" type="application/json" href="/manifest.json">
|
<link rel="manifest" type="application/json" href="/manifest.json/">
|
||||||
<link rel="shortcut icon" type="image/x-icon" href="/favicon.ico">
|
<link rel="shortcut icon" type="image/x-icon" href="/favicon.ico/">
|
||||||
<link rel="stylesheet" type="text/css" href="/style.css">
|
<link rel="stylesheet" type="text/css" href="/style.css/">
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
<body class="
|
<body class="
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div id="settings">
|
<div id="settings">
|
||||||
<form action="/settings" method="POST">
|
<form action="/settings/" method="POST">
|
||||||
<div class="prefs">
|
<div class="prefs">
|
||||||
<p>Appearance</p>
|
<p>Appearance</p>
|
||||||
<div id="theme">
|
<div id="theme">
|
||||||
@ -57,7 +57,7 @@
|
|||||||
{% for sub in prefs.subs %}
|
{% for sub in prefs.subs %}
|
||||||
<li>
|
<li>
|
||||||
<span>{{ sub }}</span>
|
<span>{{ sub }}</span>
|
||||||
<form action="/r/{{ sub }}/unsubscribe/?redirect=/settings" method="POST">
|
<form action="/r/{{ sub }}/unsubscribe/?redirect=settings" method="POST">
|
||||||
<button class="unsubscribe">Unsubscribe</button>
|
<button class="unsubscribe">Unsubscribe</button>
|
||||||
</form>
|
</form>
|
||||||
</li>
|
</li>
|
||||||
|
@ -80,11 +80,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="sub_subscription">
|
<div id="sub_subscription">
|
||||||
{% if prefs.subs.contains(sub.name) %}
|
{% if prefs.subs.contains(sub.name) %}
|
||||||
<form action="/r/{{ sub.name }}/unsubscribe" method="POST">
|
<form action="/r/{{ sub.name }}/unsubscribe/" method="POST">
|
||||||
<button class="unsubscribe">Unsubscribe</button>
|
<button class="unsubscribe">Unsubscribe</button>
|
||||||
</form>
|
</form>
|
||||||
{% else %}
|
{% else %}
|
||||||
<form action="/r/{{ sub.name }}/subscribe" method="POST">
|
<form action="/r/{{ sub.name }}/subscribe/" method="POST">
|
||||||
<button class="subscribe">Subscribe</button>
|
<button class="subscribe">Subscribe</button>
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
Loading…
Reference in New Issue
Block a user