Compare commits

..

33 Commits

Author SHA1 Message Date
9c1a932214 Clean Up Post Headers 2021-01-04 21:17:19 -08:00
8c0269af1c Fix post tags on mobile 2021-01-04 19:43:35 -08:00
df89c5076e Compact Libreddit Posts on Mobile 2021-01-04 19:26:41 -08:00
f819ad2bc6 Remove CSP "Upgrade Insecure Requests" Header 2021-01-04 10:11:07 -08:00
f5884a5270 Update Screenshot 2021-01-03 21:32:45 -08:00
c046d00060 Handle Unwrapping Errors 2021-01-03 21:31:21 -08:00
5934e34ea0 Merge pull request #30 from moosingin3space/master
Add controversial sort order
2021-01-03 21:15:27 -08:00
463b44ac52 Fix timeframe when sorting by controversial 2021-01-04 05:05:21 +00:00
b40d21e559 Add controversial sort order 2021-01-03 21:00:36 -08:00
a422a74747 Make Design More Compact 2021-01-03 19:44:44 -08:00
4124fa87d3 Correct Readme 2021-01-03 18:24:30 -08:00
1dd0c4ee20 Fix User Icon Proxy 2021-01-03 18:23:57 -08:00
0dd114c166 Post upvote ratio, permalink and reddit link 2021-01-03 13:06:49 -08:00
67090e9b08 Fix Proxied Icons 2021-01-03 10:22:41 -08:00
d97fb49fde Fix post::item IDs 2021-01-02 22:46:02 -08:00
9263b0657f Fix navbar padding 2021-01-02 22:40:22 -08:00
a3384cbaa6 Fix search pages 2021-01-02 22:37:54 -08:00
5d26b5c764 Upgrade Insecure Requests 2021-01-02 20:59:14 -08:00
516403ee47 Fix Readme 2021-01-02 20:59:04 -08:00
5ea504e6e8 Restrict Proxy to Reddit Domains 2021-01-02 20:50:23 -08:00
f49bff9853 Optimize Sequencing 2021-01-02 11:09:26 -08:00
4ec529cdb8 Rewrite Reddit Links to Libreddit 2021-01-02 10:58:21 -08:00
779de6f8af Fix Wiki Titles 2021-01-01 22:34:25 -08:00
0925a9b334 Add Wiki Pages 2021-01-01 22:21:43 -08:00
2f2ed6169d Optimize use of .unwrap() 2021-01-01 15:28:13 -08:00
59ef30c76d Remove .clone() in favor of borrowing 2021-01-01 12:55:09 -08:00
d43b49e7e4 Optimize Rust code with Clippy 2021-01-01 12:33:57 -08:00
64a92195dd Merge pull request #19 from somoso/patch-1
Fix posts overflowing on Safari on iOS
2021-01-01 11:52:21 -08:00
a7925ed62d Fix posts overflowing on Safari on iOS
In Safari, the value `anywhere` is not supported for property `overflow-wrap`. Once changed to `break-word`, it behaves like it does in Chrome and Firefox.
2021-01-01 15:46:36 +00:00
39ba50dada Error Page 2020-12-31 21:03:44 -08:00
bc1b29246d Update Screenshot 2020-12-31 20:23:19 -08:00
2d77a91150 Refactor Page Titles and Add Subreddit/User Titles 2020-12-31 20:21:56 -08:00
93c1db502d Fix Title and Navbar 2020-12-31 16:45:10 -08:00
20 changed files with 884 additions and 604 deletions

39
Cargo.lock generated
View File

@ -93,7 +93,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4ca8ce00b267af8ccebbd647de0d61e0674b6e61185cc7a592ff88772bed655" checksum = "b4ca8ce00b267af8ccebbd647de0d61e0674b6e61185cc7a592ff88772bed655"
dependencies = [ dependencies = [
"quote 1.0.8", "quote 1.0.8",
"syn 1.0.56", "syn 1.0.57",
] ]
[[package]] [[package]]
@ -267,7 +267,7 @@ checksum = "ad26f77093333e0e7c6ffe54ebe3582d908a104e448723eec6d43d08b07143fb"
dependencies = [ dependencies = [
"proc-macro2 1.0.24", "proc-macro2 1.0.24",
"quote 1.0.8", "quote 1.0.8",
"syn 1.0.56", "syn 1.0.57",
] ]
[[package]] [[package]]
@ -346,7 +346,7 @@ checksum = "e5444eec77a9ec2bfe4524139e09195862e981400c4358d3b760cae634e4c4ee"
dependencies = [ dependencies = [
"proc-macro2 1.0.24", "proc-macro2 1.0.24",
"quote 1.0.8", "quote 1.0.8",
"syn 1.0.56", "syn 1.0.57",
] ]
[[package]] [[package]]
@ -357,7 +357,7 @@ checksum = "8d3a45e77e34375a7923b1e8febb049bb011f064714a8e17a1a616fef01da13d"
dependencies = [ dependencies = [
"proc-macro2 1.0.24", "proc-macro2 1.0.24",
"quote 1.0.8", "quote 1.0.8",
"syn 1.0.56", "syn 1.0.57",
] ]
[[package]] [[package]]
@ -562,7 +562,7 @@ checksum = "41cb0e6161ad61ed084a36ba71fbba9e3ac5aee3606fb607fe08da6acbcf3d8c"
dependencies = [ dependencies = [
"proc-macro2 1.0.24", "proc-macro2 1.0.24",
"quote 1.0.8", "quote 1.0.8",
"syn 1.0.56", "syn 1.0.57",
] ]
[[package]] [[package]]
@ -604,7 +604,7 @@ dependencies = [
"heck", "heck",
"proc-macro2 1.0.24", "proc-macro2 1.0.24",
"quote 1.0.8", "quote 1.0.8",
"syn 1.0.56", "syn 1.0.57",
] ]
[[package]] [[package]]
@ -696,7 +696,7 @@ dependencies = [
"proc-macro-hack", "proc-macro-hack",
"proc-macro2 1.0.24", "proc-macro2 1.0.24",
"quote 1.0.8", "quote 1.0.8",
"syn 1.0.56", "syn 1.0.57",
] ]
[[package]] [[package]]
@ -1006,13 +1006,14 @@ checksum = "1482821306169ec4d07f6aca392a4681f66c75c9918aa49641a2595db64053cb"
[[package]] [[package]]
name = "libreddit" name = "libreddit"
version = "0.2.4" version = "0.2.6"
dependencies = [ dependencies = [
"actix-web", "actix-web",
"askama", "askama",
"async-recursion", "async-recursion",
"base64 0.13.0", "base64 0.13.0",
"chrono", "chrono",
"regex",
"reqwest", "reqwest",
"serde", "serde",
"serde_json", "serde_json",
@ -1263,7 +1264,7 @@ checksum = "65ad2ae56b6abe3a1ee25f15ee605bacadb9a764edaba9c2bf4103800d4a1895"
dependencies = [ dependencies = [
"proc-macro2 1.0.24", "proc-macro2 1.0.24",
"quote 1.0.8", "quote 1.0.8",
"syn 1.0.56", "syn 1.0.57",
] ]
[[package]] [[package]]
@ -1274,7 +1275,7 @@ checksum = "f8e8d2bf0b23038a4424865103a4df472855692821aab4e4f5c3312d461d9e5f"
dependencies = [ dependencies = [
"proc-macro2 1.0.24", "proc-macro2 1.0.24",
"quote 1.0.8", "quote 1.0.8",
"syn 1.0.56", "syn 1.0.57",
] ]
[[package]] [[package]]
@ -1563,7 +1564,7 @@ checksum = "c84d3526699cd55261af4b941e4e725444df67aa4f9e6a3564f18030d12672df"
dependencies = [ dependencies = [
"proc-macro2 1.0.24", "proc-macro2 1.0.24",
"quote 1.0.8", "quote 1.0.8",
"syn 1.0.56", "syn 1.0.57",
] ]
[[package]] [[package]]
@ -1679,7 +1680,7 @@ dependencies = [
"quote 1.0.8", "quote 1.0.8",
"serde", "serde",
"serde_derive", "serde_derive",
"syn 1.0.56", "syn 1.0.57",
] ]
[[package]] [[package]]
@ -1695,7 +1696,7 @@ dependencies = [
"serde_derive", "serde_derive",
"serde_json", "serde_json",
"sha1", "sha1",
"syn 1.0.56", "syn 1.0.57",
] ]
[[package]] [[package]]
@ -1717,9 +1718,9 @@ dependencies = [
[[package]] [[package]]
name = "syn" name = "syn"
version = "1.0.56" version = "1.0.57"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9802ddde94170d186eeee5005b798d9c159fa970403f1be19976d0cfb939b72" checksum = "4211ce9909eb971f111059df92c45640aad50a619cf55cd76476be803c4c68e6"
dependencies = [ dependencies = [
"proc-macro2 1.0.24", "proc-macro2 1.0.24",
"quote 1.0.8", "quote 1.0.8",
@ -1743,7 +1744,7 @@ checksum = "9be73a2caec27583d0046ef3796c3794f868a5bc813db689eed00c7631275cd1"
dependencies = [ dependencies = [
"proc-macro2 1.0.24", "proc-macro2 1.0.24",
"quote 1.0.8", "quote 1.0.8",
"syn 1.0.56", "syn 1.0.57",
] ]
[[package]] [[package]]
@ -1810,7 +1811,7 @@ dependencies = [
"proc-macro2 1.0.24", "proc-macro2 1.0.24",
"quote 1.0.8", "quote 1.0.8",
"standback", "standback",
"syn 1.0.56", "syn 1.0.57",
] ]
[[package]] [[package]]
@ -2093,7 +2094,7 @@ dependencies = [
"log", "log",
"proc-macro2 1.0.24", "proc-macro2 1.0.24",
"quote 1.0.8", "quote 1.0.8",
"syn 1.0.56", "syn 1.0.57",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@ -2127,7 +2128,7 @@ checksum = "b5a48c72f299d80557c7c62e37e7225369ecc0c963964059509fbafe917c7549"
dependencies = [ dependencies = [
"proc-macro2 1.0.24", "proc-macro2 1.0.24",
"quote 1.0.8", "quote 1.0.8",
"syn 1.0.56", "syn 1.0.57",
"wasm-bindgen-backend", "wasm-bindgen-backend",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]

View File

@ -3,17 +3,13 @@ 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.2.4" version = "0.2.6"
authors = ["spikecodes <19519553+spikecodes@users.noreply.github.com>"] authors = ["spikecodes <19519553+spikecodes@users.noreply.github.com>"]
edition = "2018" edition = "2018"
[features]
default = ["proxy"]
proxy = ["actix-web/rustls", "base64"]
[dependencies] [dependencies]
base64 = { version = "0.13.0", optional = true } base64 = "0.13.0"
actix-web = "3.2.0" actix-web = { version = "3.2.0", features = ["rustls"] }
reqwest = { version = "0.10", default_features = false, features = ["rustls-tls"] } reqwest = { version = "0.10", default_features = false, features = ["rustls-tls"] }
askama = "0.8.0" askama = "0.8.0"
serde = "1.0.117" serde = "1.0.117"
@ -21,3 +17,4 @@ serde_json = "1.0"
chrono = "0.4.19" chrono = "0.4.19"
async-recursion = "0.3.1" async-recursion = "0.3.1"
url = "2.2.0" url = "2.2.0"
regex = "1"

View File

@ -34,7 +34,7 @@ Like [Invidious](https://github.com/iv-org/invidious) but for Reddit. Browse the
## Screenshot ## Screenshot
![](https://i.ibb.co/hM6WPHq/image.png) ![](https://i.ibb.co/6mXqb4G/libreddit-rust.png)
## Instances ## Instances
@ -63,7 +63,7 @@ Find Libreddit on...
### Info ### Info
Libreddit hopes to provide an easier way to browse Reddit, without the ads, trackers, and bloat. Libreddit was inspired by other alternative front-ends to popular services such as [Invidious](https://github.com/iv-org/invidious) for YouTube, [Nitter](https://github.com/zedeus/nitter) for Twitter, and [Bibliogram](https://sr.ht/~cadence/bibliogram/) for Instagram. Libreddit hopes to provide an easier way to browse Reddit, without the ads, trackers, and bloat. Libreddit was inspired by other alternative front-ends to popular services such as [Invidious](https://github.com/iv-org/invidious) for YouTube, [Nitter](https://github.com/zedeus/nitter) for Twitter, and [Bibliogram](https://sr.ht/~cadence/bibliogram/) for Instagram.
Libreddit currently implements most of Reddit's functionalities but still lacks a few features that are being worked on below. Libreddit currently implements most of Reddit's (signed-out) functionalities but still lacks [a few features](https://github.com/spikecodes/libreddit/issues).
### How does it compare to Teddit? ### How does it compare to Teddit?
@ -191,11 +191,6 @@ Specify a custom address for the server by passing the `-a` or `--address` argum
libreddit --address=0.0.0.0:8111 libreddit --address=0.0.0.0:8111
``` ```
To disable the media proxy built into Libreddit, run:
```
libreddit --no-default-features
```
## Building ## Building
``` ```

View File

@ -5,6 +5,7 @@ use actix_web::{get, middleware::NormalizePath, web, App, HttpResponse, HttpServ
mod post; mod post;
mod proxy; mod proxy;
mod search; mod search;
// mod settings;
mod subreddit; mod subreddit;
mod user; mod user;
mod utils; mod utils;
@ -31,45 +32,54 @@ async fn main() -> std::io::Result<()> {
if args.len() > 1 { if args.len() > 1 {
for arg in args { for arg in args {
if arg.starts_with("--address=") || arg.starts_with("-a=") { if arg.starts_with("--address=") || arg.starts_with("-a=") {
let split: Vec<&str> = arg.split("=").collect(); let split: Vec<&str> = arg.split('=').collect();
address = split[1].to_string(); address = split[1].to_string();
} }
} }
} }
// start http server // start http server
println!("Running Libreddit v{} on {}!", env!("CARGO_PKG_VERSION"), address.clone()); println!("Running Libreddit v{} on {}!", env!("CARGO_PKG_VERSION"), &address);
HttpServer::new(|| { HttpServer::new(|| {
App::new() App::new()
// .default_service(web::get().to(subreddit::page))
// TRAILING SLASH MIDDLEWARE // TRAILING SLASH MIDDLEWARE
.wrap(NormalizePath::default()) .wrap(NormalizePath::default())
// DEFAULT SERVICE
.default_service(web::get().to(utils::error))
// GENERAL SERVICES // GENERAL SERVICES
.route("/style.css/", web::get().to(style)) .route("/style.css/", web::get().to(style))
.route("/favicon.ico/", web::get().to(|| HttpResponse::Ok())) .route("/favicon.ico/", web::get().to(HttpResponse::Ok))
.route("/robots.txt/", web::get().to(robots)) .route("/robots.txt/", web::get().to(robots))
// SETTINGS SERVICE
// .route("/settings/", web::get().to(settings::get))
// .route("/settings/save/", web::post().to(settings::set))
// PROXY SERVICE // PROXY SERVICE
.route("/proxy/{url:.*}/", web::get().to(proxy::handler)) .route("/proxy/{url:.*}/", web::get().to(proxy::handler))
// SEARCH SERVICES // SEARCH SERVICES
.route("/search/", web::get().to(search::page)) .route("/search/", web::get().to(search::find))
.route("r/{sub}/search/", web::get().to(search::page)) .route("r/{sub}/search/", web::get().to(search::find))
// USER SERVICES // USER SERVICES
.route("/u/{username}/", web::get().to(user::profile)) .route("/u/{username}/", web::get().to(user::profile))
.route("/user/{username}/", web::get().to(user::profile)) .route("/user/{username}/", web::get().to(user::profile))
// WIKI SERVICES
.route("/wiki/", web::get().to(subreddit::wiki))
.route("/wiki/{page}/", web::get().to(subreddit::wiki))
.route("/r/{sub}/wiki/", web::get().to(subreddit::wiki))
.route("/r/{sub}/wiki/{page}/", web::get().to(subreddit::wiki))
// SUBREDDIT SERVICES // SUBREDDIT SERVICES
.route("/r/{sub}/", web::get().to(subreddit::page)) .route("/r/{sub}/", web::get().to(subreddit::page))
.route("/r/{sub}/{sort}/", web::get().to(subreddit::page)) .route("/r/{sub}/{sort:hot|new|top|rising|controversial}/", web::get().to(subreddit::page))
// POPULAR SERVICES // POPULAR SERVICES
.route("/", web::get().to(subreddit::page)) .route("/", web::get().to(subreddit::page))
.route("/{sort:best|hot|new|top|rising}/", web::get().to(subreddit::page)) .route("/{sort:best|hot|new|top|rising|controversial}/", web::get().to(subreddit::page))
// POST SERVICES // POST SERVICES
.route("/{id:.{5,6}}/", web::get().to(post::item)) .route("/{id:.{5,6}}/", web::get().to(post::item))
.route("/r/{sub}/comments/{id}/{title}/", web::get().to(post::item)) .route("/r/{sub}/comments/{id}/{title}/", web::get().to(post::item))
.route("/r/{sub}/comments/{id}/{title}/{comment_id}/", web::get().to(post::item)) .route("/r/{sub}/comments/{id}/{title}/{comment_id}/", web::get().to(post::item))
}) })
.bind(address.clone()) .bind(&address)
.expect(format!("Cannot bind to the address: {}", address).as_str()) .unwrap_or_else(|_| panic!("Cannot bind to the address: {}", address))
.run() .run()
.await .await
} }

View File

@ -1,6 +1,6 @@
// CRATES // CRATES
use crate::utils::{format_num, format_url, param, request, val, Comment, ErrorTemplate, Flags, Flair, Post}; use crate::utils::{error, format_num, format_url, param, request, rewrite_url, val, Comment, Flags, Flair, Post};
use actix_web::{http::StatusCode, HttpRequest, HttpResponse, Result}; use actix_web::{HttpRequest, HttpResponse, Result};
use async_recursion::async_recursion; use async_recursion::async_recursion;
@ -16,37 +16,28 @@ struct PostTemplate {
sort: String, sort: String,
} }
pub async fn item(req: HttpRequest) -> Result<HttpResponse> { pub async fn item(req: HttpRequest) -> HttpResponse {
let path = format!("{}.json?{}&raw_json=1", req.path(), req.query_string()); let path = format!("{}.json?{}&raw_json=1", req.path(), req.query_string());
let sort = param(&path, "sort").await; let sort = param(&path, "sort");
let id = req.match_info().get("id").unwrap_or("").to_string();
// 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!(&id); dbg!(req.match_info().get("id").unwrap_or(""));
// Send a request to the url, receive JSON in response // Send a request to the url, receive JSON in response
let req = request(path.clone()).await; match request(&path).await {
// If the Reddit API returns an error, exit and send error page to user
if req.is_err() {
let s = ErrorTemplate {
message: req.err().unwrap().to_string(),
}
.render()
.unwrap();
return Ok(HttpResponse::Ok().status(StatusCode::NOT_FOUND).content_type("text/html").body(s));
} else {
// Otherwise, grab the JSON output from the request // Otherwise, grab the JSON output from the request
let res = req.unwrap(); Ok(res) => {
// Parse the JSON into Post and Comment structs // Parse the JSON into Post and Comment structs
let post = parse_post(res[0].clone()).await.unwrap(); let post = parse_post(&res[0]).await.unwrap();
let comments = parse_comments(res[1].clone()).await.unwrap(); let comments = parse_comments(&res[1]).await.unwrap();
// 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 { comments, post, sort }.render().unwrap(); let s = PostTemplate { comments, post, sort }.render().unwrap();
Ok(HttpResponse::Ok().content_type("text/html").body(s)) HttpResponse::Ok().content_type("text/html").body(s)
}
// If the Reddit API returns an error, exit and send error page to user
Err(msg) => error(msg.to_string()).await,
} }
} }
@ -55,71 +46,72 @@ async fn media(data: &serde_json::Value) -> (String, String) {
let post_type: &str; let post_type: &str;
let url = if !data["preview"]["reddit_video_preview"]["fallback_url"].is_null() { let url = if !data["preview"]["reddit_video_preview"]["fallback_url"].is_null() {
post_type = "video"; post_type = "video";
format_url(data["preview"]["reddit_video_preview"]["fallback_url"].as_str().unwrap().to_string()).await format_url(data["preview"]["reddit_video_preview"]["fallback_url"].as_str().unwrap_or_default().to_string())
} else if !data["secure_media"]["reddit_video"]["fallback_url"].is_null() { } else if !data["secure_media"]["reddit_video"]["fallback_url"].is_null() {
post_type = "video"; post_type = "video";
format_url(data["secure_media"]["reddit_video"]["fallback_url"].as_str().unwrap().to_string()).await format_url(data["secure_media"]["reddit_video"]["fallback_url"].as_str().unwrap_or_default().to_string())
} else if data["post_hint"].as_str().unwrap_or("") == "image" { } else if data["post_hint"].as_str().unwrap_or("") == "image" {
post_type = "image"; post_type = "image";
format_url(data["preview"]["images"][0]["source"]["url"].as_str().unwrap().to_string()).await format_url(data["preview"]["images"][0]["source"]["url"].as_str().unwrap_or_default().to_string())
} else { } else {
post_type = "link"; post_type = "link";
data["url"].as_str().unwrap().to_string() data["url"].as_str().unwrap_or_default().to_string()
}; };
(post_type.to_string(), url) (post_type.to_string(), url)
} }
// POSTS // POSTS
async fn parse_post(json: serde_json::Value) -> Result<Post, &'static str> { async fn parse_post(json: &serde_json::Value) -> Result<Post, &'static str> {
// Retrieve post (as opposed to comments) from JSON // Retrieve post (as opposed to comments) from JSON
let post_data: &serde_json::Value = &json["data"]["children"][0]; let post: &serde_json::Value = &json["data"]["children"][0];
// Grab UTC time as unix timestamp // Grab UTC time as unix timestamp
let unix_time: i64 = post_data["data"]["created_utc"].as_f64().unwrap().round() as i64; let unix_time: i64 = post["data"]["created_utc"].as_f64().unwrap_or_default().round() as i64;
// Parse post score // Parse post score and upvote ratio
let score = post_data["data"]["score"].as_i64().unwrap(); let score = post["data"]["score"].as_i64().unwrap_or_default();
let ratio: f64 = post["data"]["upvote_ratio"].as_f64().unwrap_or(1.0) * 100.0;
// Determine the type of media along with the media URL // Determine the type of media along with the media URL
let media = media(&post_data["data"]).await; let media = media(&post["data"]).await;
// Build a post using data parsed from Reddit post API // Build a post using data parsed from Reddit post API
let post = Post { Ok(Post {
title: val(post_data, "title").await, id: val(post, "id"),
community: val(post_data, "subreddit").await, title: val(post, "title"),
body: val(post_data, "selftext_html").await, community: val(post, "subreddit"),
author: val(post_data, "author").await, body: rewrite_url(&val(post, "selftext_html")),
author: val(post, "author"),
author_flair: Flair( author_flair: Flair(
val(post_data, "author_flair_text").await, val(post, "author_flair_text"),
val(post_data, "author_flair_background_color").await, val(post, "author_flair_background_color"),
val(post_data, "author_flair_text_color").await, val(post, "author_flair_text_color"),
), ),
url: val(post_data, "permalink").await, permalink: val(post, "permalink"),
score: format_num(score), score: format_num(score),
upvote_ratio: ratio as i64,
post_type: media.0, post_type: media.0,
flair: Flair( flair: Flair(
val(post_data, "link_flair_text").await, val(post, "link_flair_text"),
val(post_data, "link_flair_background_color").await, val(post, "link_flair_background_color"),
if val(post_data, "link_flair_text_color").await == "dark" { if val(post, "link_flair_text_color") == "dark" {
"black".to_string() "black".to_string()
} else { } else {
"white".to_string() "white".to_string()
}, },
), ),
flags: Flags { flags: Flags {
nsfw: post_data["data"]["over_18"].as_bool().unwrap_or(false), nsfw: post["data"]["over_18"].as_bool().unwrap_or(false),
stickied: post_data["data"]["stickied"].as_bool().unwrap_or(false), stickied: post["data"]["stickied"].as_bool().unwrap_or(false),
}, },
media: media.1, media: media.1,
time: Utc.timestamp(unix_time, 0).format("%b %e %Y %H:%M UTC").to_string(), time: Utc.timestamp(unix_time, 0).format("%b %e %Y %H:%M UTC").to_string(),
}; })
Ok(post)
} }
// COMMENTS // COMMENTS
#[async_recursion] #[async_recursion]
async fn parse_comments(json: serde_json::Value) -> Result<Vec<Comment>, &'static str> { async fn parse_comments(json: &serde_json::Value) -> Result<Vec<Comment>, &'static str> {
// Separate the comment JSON into a Vector of comments // Separate the comment JSON into a Vector of comments
let comment_data = json["data"]["children"].as_array().unwrap(); let comment_data = json["data"]["children"].as_array().unwrap();
@ -133,25 +125,25 @@ async fn parse_comments(json: serde_json::Value) -> Result<Vec<Comment>, &'stati
} }
let score = comment["data"]["score"].as_i64().unwrap_or(0); let score = comment["data"]["score"].as_i64().unwrap_or(0);
let body = val(comment, "body_html").await; let body = rewrite_url(&val(comment, "body_html"));
let replies: Vec<Comment> = if comment["data"]["replies"].is_object() { let replies: Vec<Comment> = if comment["data"]["replies"].is_object() {
parse_comments(comment["data"]["replies"].clone()).await.unwrap_or(Vec::new()) parse_comments(&comment["data"]["replies"]).await.unwrap_or_default()
} else { } else {
Vec::new() Vec::new()
}; };
comments.push(Comment { comments.push(Comment {
id: val(comment, "id").await, id: val(comment, "id"),
body: body, body,
author: val(comment, "author").await, author: val(comment, "author"),
score: format_num(score), score: format_num(score),
time: Utc.timestamp(unix_time, 0).format("%b %e %Y %H:%M UTC").to_string(), time: Utc.timestamp(unix_time, 0).format("%b %e %Y %H:%M UTC").to_string(),
replies: replies, replies,
flair: Flair( flair: Flair(
val(comment, "author_flair_text").await, val(comment, "author_flair_text"),
val(comment, "author_flair_background_color").await, val(comment, "author_flair_background_color"),
val(comment, "author_flair_text_color").await, val(comment, "author_flair_text_color"),
), ),
}); });
} }

View File

@ -1,30 +1,46 @@
use actix_web::{client::Client, web, Error, HttpResponse, Result}; use actix_web::{client::Client, error, web, Error, HttpResponse, Result};
use url::Url;
#[cfg(feature = "proxy")]
use base64::decode; use base64::decode;
pub async fn handler(web::Path(url): web::Path<String>) -> Result<HttpResponse> { pub async fn handler(web::Path(b64): web::Path<String>) -> Result<HttpResponse> {
if cfg!(feature = "proxy") { let domains = vec![
#[cfg(feature = "proxy")] // THUMBNAILS
let media: String; "a.thumbs.redditmedia.com",
"b.thumbs.redditmedia.com",
// ICONS
"styles.redditmedia.com",
"www.redditstatic.com",
// PREVIEWS
"preview.redd.it",
"external-preview.redd.it",
// MEDIA
"i.redd.it",
"v.redd.it",
];
#[cfg(not(feature = "proxy"))] match decode(b64) {
let media = url; Ok(bytes) => {
let media = String::from_utf8(bytes).unwrap_or_default();
#[cfg(feature = "proxy")] match Url::parse(media.as_str()) {
match decode(url) { Ok(url) => {
Ok(bytes) => media = String::from_utf8(bytes).unwrap(), let domain = url.domain().unwrap_or_default();
Err(_e) => return Ok(HttpResponse::Ok().body("")),
};
let client = Client::default(); if domains.contains(&domain) {
client Client::default()
.get(media.replace("&amp;", "&")) .get(media.replace("&amp;", "&"))
.send() .send()
.await .await
.map_err(Error::from) .map_err(Error::from)
.and_then(|res| Ok(HttpResponse::build(res.status()).streaming(res))) .map(|res| HttpResponse::build(res.status()).streaming(res))
} else { } else {
Ok(HttpResponse::Ok().body("")) Err(error::ErrorForbidden("Resource must be from Reddit"))
}
}
Err(_) => Err(error::ErrorBadRequest("Can't parse encoded base64 URL")),
}
}
Err(_) => Err(error::ErrorBadRequest("Can't decode base64 URL")),
} }
} }

View File

@ -1,52 +1,53 @@
// CRATES // CRATES
use crate::utils::{fetch_posts, param, ErrorTemplate, Post}; use crate::utils::{error, fetch_posts, param, Post};
use actix_web::{http::StatusCode, HttpRequest, HttpResponse, Result}; use actix_web::{HttpRequest, HttpResponse};
use askama::Template; use askama::Template;
// STRUCTS // STRUCTS
struct SearchParams {
q: String,
sort: String,
t: String,
before: String,
after: String,
restrict_sr: String,
}
#[derive(Template)] #[derive(Template)]
#[allow(dead_code)]
#[template(path = "search.html", escape = "none")] #[template(path = "search.html", escape = "none")]
struct SearchTemplate { struct SearchTemplate {
posts: Vec<Post>, posts: Vec<Post>,
query: String,
sub: String, sub: String,
sort: (String, String), params: SearchParams,
ends: (String, String),
} }
// SERVICES // SERVICES
pub async fn page(req: HttpRequest) -> Result<HttpResponse> { pub async fn find(req: HttpRequest) -> HttpResponse {
let path = format!("{}.json?{}", req.path(), req.query_string()); let path = format!("{}.json?{}", req.path(), req.query_string());
let q = param(&path, "q").await; let sort = if param(&path, "sort").is_empty() {
let sort = if param(&path, "sort").await.is_empty() {
"relevance".to_string() "relevance".to_string()
} else { } else {
param(&path, "sort").await param(&path, "sort")
}; };
let sub = req.match_info().get("sub").unwrap_or("").to_string(); let sub = req.match_info().get("sub").unwrap_or("").to_string();
let posts = fetch_posts(path.clone(), String::new()).await; match fetch_posts(&path, String::new()).await {
Ok(posts) => HttpResponse::Ok().content_type("text/html").body(
if posts.is_err() { SearchTemplate {
let s = ErrorTemplate { posts: posts.0,
message: posts.err().unwrap().to_string(), sub,
params: SearchParams {
q: param(&path, "q"),
sort,
t: param(&path, "t"),
before: param(&path, "after"),
after: posts.1,
restrict_sr: param(&path, "restrict_sr"),
},
} }
.render() .render()
.unwrap(); .unwrap(),
Ok(HttpResponse::Ok().status(StatusCode::NOT_FOUND).content_type("text/html").body(s)) ),
} else { Err(msg) => error(msg.to_string()).await,
let items = posts.unwrap();
let s = SearchTemplate {
posts: items.0,
query: q,
sub: sub,
sort: (sort, param(&path, "t").await),
ends: (param(&path, "after").await, items.1),
}
.render()
.unwrap();
Ok(HttpResponse::Ok().content_type("text/html").body(s))
} }
} }

48
src/settings.rs Normal file
View File

@ -0,0 +1,48 @@
// // CRATES
// use crate::utils::cookies;
// use actix_web::{cookie::Cookie, web::Form, HttpRequest, HttpResponse, Result}; // http::Method,
// use askama::Template;
// // STRUCTS
// #[derive(Template)]
// #[template(path = "settings.html", escape = "none")]
// struct SettingsTemplate {
// pref_nsfw: String,
// }
// #[derive(serde::Deserialize)]
// pub struct Preferences {
// pref_nsfw: Option<String>,
// }
// // FUNCTIONS
// // Retrieve cookies from request "Cookie" header
// pub async fn get(req: HttpRequest) -> Result<HttpResponse> {
// let cookies = cookies(req);
// let pref_nsfw: String = cookies.get("pref_nsfw").unwrap_or(&String::new()).to_owned();
// let s = SettingsTemplate { pref_nsfw }.render().unwrap();
// Ok(HttpResponse::Ok().content_type("text/html").body(s))
// }
// // Set cookies using response "Set-Cookie" header
// pub async fn set(form: Form<Preferences>) -> HttpResponse {
// let nsfw: Cookie = match &form.pref_nsfw {
// Some(value) => Cookie::build("pref_nsfw", value).path("/").secure(true).http_only(true).finish(),
// None => Cookie::build("pref_nsfw", "").finish(),
// };
// let body = SettingsTemplate {
// pref_nsfw: form.pref_nsfw.clone().unwrap_or_default(),
// }
// .render()
// .unwrap();
// HttpResponse::Found()
// .content_type("text/html")
// .set_header("Set-Cookie", nsfw.to_string())
// .set_header("Location", "/settings")
// .body(body)
// }

View File

@ -1,8 +1,7 @@
// CRATES // CRATES
use crate::utils::{fetch_posts, format_num, format_url, param, request, val, ErrorTemplate, Post, Subreddit}; use crate::utils::{error, fetch_posts, format_num, format_url, param, request, rewrite_url, val, Post, Subreddit};
use actix_web::{http::StatusCode, HttpRequest, HttpResponse, Result}; use actix_web::{HttpRequest, HttpResponse, Result};
use askama::Template; use askama::Template;
use std::convert::TryInto;
// STRUCTS // STRUCTS
#[derive(Template)] #[derive(Template)]
@ -14,80 +13,93 @@ struct SubredditTemplate {
ends: (String, String), ends: (String, String),
} }
#[derive(Template)]
#[template(path = "wiki.html", escape = "none")]
struct WikiTemplate {
sub: String,
wiki: String,
page: String,
}
// SERVICES // SERVICES
// web::Path(sub): web::Path<String>, params: web::Query<Params> pub async fn page(req: HttpRequest) -> HttpResponse {
pub async fn page(req: HttpRequest) -> Result<HttpResponse> {
let path = format!("{}.json?{}", req.path(), req.query_string()); let path = format!("{}.json?{}", req.path(), req.query_string());
let sub = req.match_info().get("sub").unwrap_or("popular").to_string(); let sub = req.match_info().get("sub").unwrap_or("popular").to_string();
let sort = req.match_info().get("sort").unwrap_or("hot").to_string(); let sort = req.match_info().get("sort").unwrap_or("hot").to_string();
let sub_result = if !&sub.contains("+") && sub != "popular" { let sub_result = if !&sub.contains('+') && sub != "popular" {
subreddit(&sub).await subreddit(&sub).await.unwrap_or_default()
} else { } else {
Ok(Subreddit::default()) Subreddit::default()
}; };
let posts = fetch_posts(path.clone(), String::new()).await;
if posts.is_err() {
let s = ErrorTemplate {
message: posts.err().unwrap().to_string(),
}
.render()
.unwrap();
Ok(HttpResponse::Ok().status(StatusCode::NOT_FOUND).content_type("text/html").body(s))
} else {
let sub = sub_result.unwrap_or(Subreddit::default());
let items = posts.unwrap();
match fetch_posts(&path, String::new()).await {
Ok(items) => {
let s = SubredditTemplate { let s = SubredditTemplate {
sub: sub, sub: sub_result,
posts: items.0, posts: items.0,
sort: (sort, param(&path, "t").await), sort: (sort, param(&path, "t")),
ends: (param(&path, "after").await, items.1), ends: (param(&path, "after"), items.1),
} }
.render() .render()
.unwrap(); .unwrap();
Ok(HttpResponse::Ok().content_type("text/html").body(s)) HttpResponse::Ok().content_type("text/html").body(s)
}
Err(msg) => error(msg.to_string()).await,
}
}
pub async fn wiki(req: HttpRequest) -> HttpResponse {
let sub = req.match_info().get("sub").unwrap_or("reddit.com");
let page = req.match_info().get("page").unwrap_or("index");
let path: String = format!("r/{}/wiki/{}.json?raw_json=1", sub, page);
match request(&path).await {
Ok(res) => {
let s = WikiTemplate {
sub: sub.to_string(),
wiki: rewrite_url(res["data"]["content_html"].as_str().unwrap_or_default()),
page: page.to_string(),
}
.render()
.unwrap();
HttpResponse::Ok().content_type("text/html").body(s)
}
Err(msg) => error(msg.to_string()).await,
} }
} }
// SUBREDDIT // SUBREDDIT
async fn subreddit(sub: &String) -> Result<Subreddit, &'static str> { async fn subreddit(sub: &str) -> Result<Subreddit, &'static str> {
// Build the Reddit JSON API url // Build the Reddit JSON API url
let url: 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, receive JSON in response
let req = request(url).await;
// If the Reddit API returns an error, exit this function
if req.is_err() {
return Err(req.err().unwrap());
}
// Otherwise, grab the JSON output from the request
let res = req.unwrap();
// Send a request to the url
match request(&path).await {
// If success, receive JSON in response
Ok(res) => {
// Metadata regarding the subreddit // Metadata regarding the subreddit
let members = res["data"]["subscribers"].as_u64().unwrap_or(0); let members: i64 = res["data"]["subscribers"].as_u64().unwrap_or_default() as i64;
let active = res["data"]["accounts_active"].as_u64().unwrap_or(0); 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().unwrap().split("?").collect::<Vec<&str>>()[0]; let community_icon: &str = res["data"]["community_icon"].as_str().unwrap_or("").split('?').collect::<Vec<&str>>()[0];
let icon = if community_icon.is_empty() { let icon = if community_icon.is_empty() { val(&res, "icon_img") } else { community_icon.to_string() };
val(&res, "icon_img").await
} else {
community_icon.to_string()
};
let sub = Subreddit { let sub = Subreddit {
name: val(&res, "display_name").await, name: val(&res, "display_name"),
title: val(&res, "title").await, title: val(&res, "title"),
description: val(&res, "public_description").await, description: val(&res, "public_description"),
info: val(&res, "description_html").await.replace("\\", ""), info: rewrite_url(&val(&res, "description_html").replace("\\", "")),
icon: format_url(icon).await, icon: format_url(icon),
members: format_num(members.try_into().unwrap()), members: format_num(members),
active: format_num(active.try_into().unwrap()), active: format_num(active),
wiki: res["data"]["wiki_enabled"].as_bool().unwrap_or_default(),
}; };
Ok(sub) Ok(sub)
} }
// If the Reddit API returns an error, exit this function
Err(msg) => return Err(msg),
}
}

View File

@ -1,6 +1,6 @@
// CRATES // CRATES
use crate::utils::{fetch_posts, format_url, nested_val, param, request, ErrorTemplate, Post, User}; use crate::utils::{error, fetch_posts, format_url, nested_val, param, request, Post, User};
use actix_web::{http::StatusCode, HttpRequest, HttpResponse, Result}; use actix_web::{HttpRequest, HttpResponse, Result};
use askama::Template; use askama::Template;
use chrono::{TimeZone, Utc}; use chrono::{TimeZone, Utc};
@ -14,72 +14,64 @@ struct UserTemplate {
ends: (String, String), ends: (String, String),
} }
pub async fn profile(req: HttpRequest) -> Result<HttpResponse> { // FUNCTIONS
pub async fn profile(req: HttpRequest) -> HttpResponse {
// 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.path(), req.query_string());
// Retrieve other variables from Libreddit request // Retrieve other variables from Libreddit request
let sort = param(&path, "sort").await; let sort = param(&path, "sort");
let username = req.match_info().get("username").unwrap_or("").to_string(); let username = req.match_info().get("username").unwrap_or("").to_string();
// Request user profile data and user posts/comments from Reddit // Request user profile data and user posts/comments from Reddit
let user = user(&username).await; let user = user(&username).await;
let posts = fetch_posts(path.clone(), "Comment".to_string()).await; let posts = fetch_posts(&path, "Comment".to_string()).await;
// If there is an error show error page
if user.is_err() || posts.is_err() {
let s = ErrorTemplate {
message: user.err().unwrap().to_string(),
}
.render()
.unwrap();
Ok(HttpResponse::Ok().status(StatusCode::NOT_FOUND).content_type("text/html").body(s))
} else {
let posts_unwrapped = posts.unwrap();
match posts {
Ok(items) => {
let s = UserTemplate { let s = UserTemplate {
user: user.unwrap(), user: user.unwrap(),
posts: posts_unwrapped.0, posts: items.0,
sort: (sort, param(&path, "t").await), sort: (sort, param(&path, "t")),
ends: (param(&path, "after").await, posts_unwrapped.1), ends: (param(&path, "after"), items.1),
} }
.render() .render()
.unwrap(); .unwrap();
Ok(HttpResponse::Ok().content_type("text/html").body(s)) HttpResponse::Ok().content_type("text/html").body(s)
}
// If there is an error show error page
Err(msg) => error(msg.to_string()).await,
} }
} }
// SERVICES
// pub async fn page(web::Path(username): web::Path<String>, params: web::Query<Params>) -> Result<HttpResponse> {
// render(username, params.sort.clone(), params.t.clone(), (params.before.clone(), params.after.clone())).await
// }
// USER // USER
async fn user(name: &String) -> Result<User, &'static str> { async fn user(name: &str) -> Result<User, &'static str> {
// Build the Reddit JSON API url // Build the Reddit JSON API path
let url: String = format!("user/{}/about.json", name); let path: String = format!("user/{}/about.json", name);
// Send a request to the url, receive JSON in response let res;
let req = request(url).await;
// Send a request to the url
match request(&path).await {
// If success, receive JSON in response
Ok(response) => {
res = response;
}
// If the Reddit API returns an error, exit this function // If the Reddit API returns an error, exit this function
if req.is_err() { Err(msg) => return Err(msg),
return Err(req.err().unwrap());
} }
// Otherwise, grab the JSON output from the request
let res = req.unwrap();
// Grab creation date as unix timestamp // Grab creation date as unix timestamp
let created: i64 = res["data"]["created"].as_f64().unwrap().round() as i64; let created: i64 = res["data"]["created"].as_f64().unwrap_or(0.0).round() as i64;
// Parse the JSON output into a User struct // Parse the JSON output into a User struct
Ok(User { Ok(User {
name: name.to_string(), name: name.to_string(),
icon: format_url(nested_val(&res, "subreddit", "icon_img").await).await, title: nested_val(&res, "subreddit", "title"),
karma: res["data"]["total_karma"].as_i64().unwrap(), icon: format_url(nested_val(&res, "subreddit", "icon_img")),
karma: res["data"]["total_karma"].as_i64().unwrap_or(0),
created: Utc.timestamp(created, 0).format("%b %e, %Y").to_string(), created: Utc.timestamp(created, 0).format("%b %e, %Y").to_string(),
banner: nested_val(&res, "subreddit", "banner_img").await, banner: nested_val(&res, "subreddit", "banner_img"),
description: nested_val(&res, "subreddit", "public_description").await, description: nested_val(&res, "subreddit", "public_description"),
}) })
} }

View File

@ -1,14 +1,17 @@
// use std::collections::HashMap;
// //
// CRATES // CRATES
// //
use actix_web::{HttpResponse, Result};
use askama::Template;
use base64::encode;
use chrono::{TimeZone, Utc}; use chrono::{TimeZone, Utc};
use serde_json::{from_str, Value}; use regex::Regex;
use serde_json::from_str;
use url::Url; use url::Url;
// use surf::{client, get, middleware::Redirect}; // use surf::{client, get, middleware::Redirect};
#[cfg(feature = "proxy")]
use base64::encode;
// //
// STRUCTS // STRUCTS
// //
@ -22,13 +25,15 @@ pub struct Flags {
// Post containing content, metadata and media // Post containing content, metadata and media
pub struct Post { pub struct Post {
pub id: String,
pub title: String, pub title: String,
pub community: String, pub community: String,
pub body: String, pub body: String,
pub author: String, pub author: String,
pub author_flair: Flair, pub author_flair: Flair,
pub url: String, pub permalink: String,
pub score: String, pub score: String,
pub upvote_ratio: i64,
pub post_type: String, pub post_type: String,
pub flair: Flair, pub flair: Flair,
pub flags: Flags, pub flags: Flags,
@ -50,6 +55,7 @@ pub struct Comment {
// User struct containing metadata about user // User struct containing metadata about user
pub struct User { pub struct User {
pub name: String, pub name: String,
pub title: String,
pub icon: String, pub icon: String,
pub karma: i64, pub karma: i64,
pub created: String, pub created: String,
@ -67,6 +73,7 @@ pub struct Subreddit {
pub icon: String, pub icon: String,
pub members: String, pub members: String,
pub active: String, pub active: String,
pub wiki: bool,
} }
// Parser for query params, used in sorting (eg. /r/rust/?sort=hot) // Parser for query params, used in sorting (eg. /r/rust/?sort=hot)
@ -80,7 +87,7 @@ pub struct Params {
} }
// Error template // Error template
#[derive(askama::Template)] #[derive(Template)]
#[template(path = "error.html", escape = "none")] #[template(path = "error.html", escape = "none")]
pub struct ErrorTemplate { pub struct ErrorTemplate {
pub message: String, pub message: String,
@ -91,23 +98,43 @@ pub struct ErrorTemplate {
// //
// Grab a query param from a url // Grab a query param from a url
pub async fn param(path: &String, value: &str) -> String { pub fn param(path: &str, value: &str) -> String {
let url = Url::parse(format!("https://reddit.com/{}", path).as_str()).unwrap(); let url = Url::parse(format!("https://libredd.it/{}", path).as_str()).unwrap();
let pairs: std::collections::HashMap<_, _> = url.query_pairs().into_owned().collect(); let pairs: std::collections::HashMap<_, _> = url.query_pairs().into_owned().collect();
pairs.get(value).unwrap_or(&String::new()).to_owned() pairs.get(value).unwrap_or(&String::new()).to_owned()
} }
// Cookies from request
// pub fn cookies(req: HttpRequest) -> HashMap<String, String> {
// let mut result: HashMap<String, String> = HashMap::new();
// let cookies: Vec<Cookie> = req
// .headers()
// .get_all("Cookie")
// .map(|value| value.to_str().unwrap())
// .map(|unparsed| Cookie::parse(unparsed).unwrap())
// .collect();
// for cookie in cookies {
// result.insert(cookie.name().to_string(), cookie.value().to_string());
// }
// result
// }
// Direct urls to proxy if proxy is enabled // Direct urls to proxy if proxy is enabled
pub async fn format_url(url: String) -> String { pub fn format_url(url: String) -> String {
if url.is_empty() { if url.is_empty() || url == "self" || url == "default" {
return String::new(); String::new()
}; } else {
format!("/proxy/{}", encode(url).as_str())
}
}
#[cfg(feature = "proxy")] // Rewrite Reddit links to Libreddit in body of text
return "/proxy/".to_string() + encode(url).as_str(); pub fn rewrite_url(text: &str) -> String {
let re = Regex::new(r#"href="(https://|http://|)(www.|)(reddit).(com)/"#).unwrap();
#[cfg(not(feature = "proxy"))] re.replace_all(text, r#"href="/"#).to_string()
return url.to_string();
} }
// Append `m` and `k` for millions and thousands respectively // Append `m` and `k` for millions and thousands respectively
@ -126,123 +153,124 @@ pub fn format_num(num: i64) -> String {
// //
// val() function used to parse JSON from Reddit APIs // val() function used to parse JSON from Reddit APIs
pub async fn val(j: &serde_json::Value, k: &str) -> String { pub fn val(j: &serde_json::Value, k: &str) -> String {
String::from(j["data"][k].as_str().unwrap_or("")) String::from(j["data"][k].as_str().unwrap_or_default())
} }
// nested_val() function used to parse JSON from Reddit APIs // nested_val() function used to parse JSON from Reddit APIs
pub async fn nested_val(j: &serde_json::Value, n: &str, k: &str) -> String { pub fn nested_val(j: &serde_json::Value, n: &str, k: &str) -> String {
String::from(j["data"][n][k].as_str().unwrap()) String::from(j["data"][n][k].as_str().unwrap_or_default())
} }
// Fetch posts of a user or subreddit // Fetch posts of a user or subreddit
pub async fn fetch_posts(path: String, fallback_title: String) -> Result<(Vec<Post>, String), &'static str> { pub async fn fetch_posts(path: &str, fallback_title: String) -> Result<(Vec<Post>, String), &'static str> {
// Send a request to the url, receive JSON in response let res;
let req = request(path.clone()).await; let post_list;
// Send a request to the url
match request(&path).await {
// If success, receive JSON in response
Ok(response) => {
res = response;
}
// If the Reddit API returns an error, exit this function // If the Reddit API returns an error, exit this function
if req.is_err() { Err(msg) => return Err(msg),
return Err(req.err().unwrap());
} }
// Otherwise, grab the JSON output from the request
let res = req.unwrap();
// Fetch the list of posts from the JSON response // Fetch the list of posts from the JSON response
let post_list = res["data"]["children"].as_array().unwrap(); match res["data"]["children"].as_array() {
Some(list) => post_list = list,
None => return Err("No posts found"),
}
let mut posts: Vec<Post> = Vec::new(); let mut posts: Vec<Post> = Vec::new();
// For each post from posts list
for post in post_list { for post in post_list {
let img = if val(post, "thumbnail").await.starts_with("https:/") { let img = format_url(val(post, "thumbnail"));
format_url(val(post, "thumbnail").await).await let unix_time: i64 = post["data"]["created_utc"].as_f64().unwrap_or_default().round() as i64;
} else { let score = post["data"]["score"].as_i64().unwrap_or_default();
String::new() let ratio: f64 = post["data"]["upvote_ratio"].as_f64().unwrap_or(1.0) * 100.0;
}; let title = val(post, "title");
let unix_time: i64 = post["data"]["created_utc"].as_f64().unwrap().round() as i64;
let score = post["data"]["score"].as_i64().unwrap();
let title = val(post, "title").await;
posts.push(Post { posts.push(Post {
id: val(post, "id"),
title: if title.is_empty() { fallback_title.to_owned() } else { title }, title: if title.is_empty() { fallback_title.to_owned() } else { title },
community: val(post, "subreddit").await, community: val(post, "subreddit"),
body: val(post, "body_html").await, body: rewrite_url(&val(post, "body_html")),
author: val(post, "author").await, author: val(post, "author"),
author_flair: Flair( author_flair: Flair(
val(post, "author_flair_text").await, val(post, "author_flair_text"),
val(post, "author_flair_background_color").await, val(post, "author_flair_background_color"),
val(post, "author_flair_text_color").await, val(post, "author_flair_text_color"),
), ),
score: format_num(score), score: format_num(score),
upvote_ratio: ratio as i64,
post_type: "link".to_string(), post_type: "link".to_string(),
media: img, media: img,
flair: Flair( flair: Flair(
val(post, "link_flair_text").await, val(post, "link_flair_text"),
val(post, "link_flair_background_color").await, val(post, "link_flair_background_color"),
if val(post, "link_flair_text_color").await == "dark" { if val(post, "link_flair_text_color") == "dark" {
"black".to_string() "black".to_string()
} else { } else {
"white".to_string() "white".to_string()
}, },
), ),
flags: Flags { flags: Flags {
nsfw: post["data"]["over_18"].as_bool().unwrap_or(false), nsfw: post["data"]["over_18"].as_bool().unwrap_or_default(),
stickied: post["data"]["stickied"].as_bool().unwrap_or(false), stickied: post["data"]["stickied"].as_bool().unwrap_or_default(),
}, },
url: val(post, "permalink").await, permalink: val(post, "permalink"),
time: Utc.timestamp(unix_time, 0).format("%b %e '%y").to_string(), time: Utc.timestamp(unix_time, 0).format("%b %e '%y").to_string(),
}); });
} }
dbg!(path); Ok((posts, res["data"]["after"].as_str().unwrap_or_default().to_string()))
Ok((posts, res["data"]["after"].as_str().unwrap_or("").to_string()))
} }
// //
// NETWORKING // NETWORKING
// //
pub async fn error(msg: String) -> HttpResponse {
let body = ErrorTemplate { message: msg }.render().unwrap_or_default();
HttpResponse::NotFound().content_type("text/html").body(body)
}
// Make a request to a Reddit API and parse the JSON response // Make a request to a Reddit API and parse the JSON response
pub async fn request(path: String) -> Result<serde_json::Value, &'static str> { pub async fn request(path: &str) -> Result<serde_json::Value, &'static str> {
let url = format!("https://www.reddit.com/{}", path); let url = format!("https://www.reddit.com/{}", path);
// --- actix-web::client --- // Send request using reqwest
// let client = actix_web::client::Client::default(); match reqwest::get(&url).await {
// let res = client Ok(res) => {
// .get(url)
// .send()
// .await?
// .body()
// .limit(1000000)
// .await?;
// let body = std::str::from_utf8(res.as_ref())?; // .as_ref converts Bytes to [u8]
// --- surf ---
// let req = get(&url).header("User-Agent", "libreddit");
// let client = client().with(Redirect::new(5));
// let mut res = client.send(req).await.unwrap();
// let success = res.status().is_success();
// let body = res.body_string().await.unwrap();
// --- reqwest ---
let res = reqwest::get(&url).await.unwrap();
// Read the status from the response // Read the status from the response
let success = res.status().is_success(); match res.status().is_success() {
// Read the body of the response true => {
let body = res.text().await.unwrap();
// Parse the response from Reddit as JSON // Parse the response from Reddit as JSON
let json: Value = from_str(body.as_str()).unwrap_or(Value::Null); match from_str(res.text().await.unwrap_or_default().as_str()) {
Ok(json) => Ok(json),
if !success { Err(_) => {
println!("! {} - {}", url, "Page not found"); #[cfg(debug_assertions)]
Err("Page not found") dbg!(format!("{} - Failed to parse page JSON data", url));
} else if json == Value::Null {
println!("! {} - {}", url, "Failed to parse page JSON data");
Err("Failed to parse page JSON data") Err("Failed to parse page JSON data")
} else { }
Ok(json) }
}
// If Reddit returns error, tell user Page Not Found
false => {
#[cfg(debug_assertions)]
dbg!(format!("{} - Page not found", url));
Err("Page not found")
}
}
}
// If can't send request to Reddit, return this to user
Err(e) => {
#[cfg(debug_assertions)]
dbg!(format!("{} - {}", url, e));
Err("Couldn't send request to Reddit")
}
} }
} }

View File

@ -16,7 +16,6 @@
} }
* { * {
transition: 0.2s all;
margin: 0; margin: 0;
color: white; color: white;
font-family: sans-serif; font-family: sans-serif;
@ -24,7 +23,7 @@
body { body {
background: var(--background); background: var(--background);
visibility: visible !important; font-size: 15px;
} }
nav { nav {
@ -35,7 +34,7 @@ nav {
background: var(--outside); background: var(--outside);
padding: 5px 15px; padding: 5px 15px;
font-size: 20px; font-size: 20px;
height: 40px; min-height: 40px;
} }
nav #lib, nav #github, nav #version { color: white; } nav #lib, nav #github, nav #version { color: white; }
@ -44,11 +43,17 @@ nav #version { opacity: 25%; }
main { main {
display: flex; display: flex;
justify-content: center; justify-content: center;
max-width: 750px; max-width: 1000px;
padding: 10px 20px; padding: 10px 20px;
margin: 20px auto; margin: 20px auto;
} }
#column_one {
max-width: 750px;
border-radius: 5px;
overflow: hidden;
}
footer { footer {
display: flex; display: flex;
justify-content: center; justify-content: center;
@ -71,6 +76,7 @@ hr {
a { a {
color: inherit; color: inherit;
text-decoration: none; text-decoration: none;
transition: 0.2s all;
} }
a:not(.post_right):hover { a:not(.post_right):hover {
@ -83,10 +89,19 @@ img[src=""] {
aside { aside {
flex-grow: 1; flex-grow: 1;
margin: 20px 20px 0 20px; margin: 20px 20px 0 10px;
max-width: 350px; max-width: 350px;
} }
.panel {
border: 1px solid var(--highlighted);
}
.dot {
font-size: 12px;
opacity: 0.5;
}
/* User & Subreddit */ /* User & Subreddit */
#user, #subreddit, #sidebar { #user, #subreddit, #sidebar {
@ -94,22 +109,20 @@ aside {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
padding: 20px;
height: max-content; height: max-content;
background: var(--outside); background: var(--outside);
border-radius: 5px; border-radius: 5px;
overflow: hidden;
} }
#sidebar, #sidebar_contents { #user *, #subreddit * { text-align: center; }
margin-top: 20px;
}
#sidebar_label { #user, #sub_meta, #sidebar_contents { padding: 20px; }
border: 2px solid var(--highlighted);
padding: 10px;
}
#user_icon, #subreddit_icon { #sidebar, #sidebar_contents { margin-top: 10px; }
#sidebar_label { padding: 10px; }
#user_icon, #sub_icon {
width: 100px; width: 100px;
height: 100px; height: 100px;
border: 2px solid var(--accent); border: 2px solid var(--accent);
@ -118,39 +131,66 @@ aside {
margin: 10px; margin: 10px;
} }
#user_name, #subreddit_name { #user_title, #sub_title {
margin-top: 10px; margin: 0 20px;
font-size: 20px;
font-weight: bold;
} }
#user_description, #subreddit_description { #user_description, #sub_description {
margin: 10px 20px; margin: 0 20px;
text-align: center;
font-size: 15px;
} }
#user_details, #subreddit_details { #user_name, #user_description:not(:empty), #user_icon
#sub_name, #sub_icon, #sub_description:not(:empty) {
margin-bottom: 20px;
}
#user_details, #sub_details {
display: grid; display: grid;
grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(2, 1fr);
margin-top: 15px;
grid-column-gap: 20px; grid-column-gap: 20px;
} }
#user_details > label, #subreddit_details > label { #user_details > label, #sub_details > label {
color: var(--accent); color: var(--accent);
font-size: 15px; }
/* Wiki Pages */
#wiki {
background: var(--foreground);
padding: 35px;
}
#top {
background: var(--highlighted);
width: 100%;
display: flex;
}
#top > * {
flex-grow: 1;
text-align: center;
height: 35px;
line-height: 35px;
}
#top > div {
border-bottom: 2px solid white;
} }
/* Sorting and Search */ /* Sorting and Search */
select { select {
background: var(--outside); background: var(--outside);
transition: 0.2s all;
} }
select, #search { select, #search {
border: 0; border: none;
padding: 0 15px; padding: 0 15px;
height: 40px; height: 40px;
font-size: 15px;
appearance: none; appearance: none;
border-radius: 5px 0px 0px 5px; border-radius: 5px 0px 0px 5px;
} }
@ -173,7 +213,6 @@ select, #search {
#inside { #inside {
display: flex; display: flex;
font-size: 15px;
align-items: center; align-items: center;
border-right: 2px var(--outside) solid; border-right: 2px var(--outside) solid;
height: 40px; height: 40px;
@ -185,20 +224,26 @@ select, #search {
input[type="submit"] { input[type="submit"] {
border: 0; border: 0;
border-radius: 0px 5px 5px 0px; border-radius: 0px 5px 5px 0px;
transition: 0.2s all;
} }
select:hover { background: var(--foreground); } select:hover { background: var(--foreground); }
input[type="submit"]:hover { color: var(--accent); } input[type="submit"]:hover { color: var(--accent); }
#timeframe { #timeframe {
border-radius: 5px 0px 0px 5px; margin: 0 2px;
border-radius: 0;
}
#sort_options + #timeframe:not(#search_sort > #timeframe) {
margin-left: 10px; margin-left: 10px;
border-radius: 5px 0px 0px 5px;
} }
#search_sort { #search_sort {
background: var(--highlighted); background: var(--highlighted);
border-radius: 5px; border-radius: 5px;
overflow: hidden; overflow: auto;
} }
#search_sort > #search { #search_sort > #search {
@ -206,6 +251,8 @@ input[type="submit"]:hover { color: var(--accent); }
background: transparent; background: transparent;
} }
#search_sort > *, #searchbox > * { font-size: 15px; }
#search_sort > :not(:first-child), #search_sort > #sort_options { #search_sort > :not(:first-child), #search_sort > #sort_options {
margin: 0; margin: 0;
border-radius: 0; border-radius: 0;
@ -230,7 +277,7 @@ input[type="submit"]:hover { color: var(--accent); }
box-shadow: var(--black-contrast); box-shadow: var(--black-contrast);
background: var(--outside); background: var(--outside);
display: flex; display: flex;
overflow: hidden; overflow: auto;
} }
#sort_options > a, footer > a { #sort_options > a, footer > a {
@ -238,6 +285,7 @@ input[type="submit"]:hover { color: var(--accent); }
padding: 10px 20px; padding: 10px 20px;
text-align: center; text-align: center;
cursor: pointer; cursor: pointer;
transition: 0.2s all;
} }
#sort_options > a.selected { #sort_options > a.selected {
@ -256,12 +304,19 @@ input[type="submit"]:hover { color: var(--accent); }
background: var(--post); background: var(--post);
box-shadow: var(--black-contrast); box-shadow: var(--black-contrast);
display: flex; display: flex;
transition: 0.2s all;
} }
.post:not(:last-child) { margin-bottom: 10px; }
.post.highlighted { .post.highlighted {
margin: 20px 0; margin: 20px 0;
} }
.post.highlighted > .post_right {
flex-direction: column;
}
.post:hover { .post:hover {
background: var(--foreground); background: var(--foreground);
} }
@ -272,22 +327,100 @@ input[type="submit"]:hover { color: var(--accent); }
.post_left, .post_right { .post_left, .post_right {
display: flex; display: flex;
flex-direction: column; overflow-wrap: break-word;
overflow-wrap: anywhere;
} }
.post_left { .post_left {
text-align: center; text-align: center;
background: var(--foreground); background: var(--foreground);
border-radius: 5px 0 0 5px; border-radius: 5px 0 0 5px;
flex-direction: column;
min-width: 50px; min-width: 50px;
padding: 5px; transition: 0.2s all;
} }
.post_score { .post_score {
margin-top: 20px; margin-top: 20px;
color: var(--accent); color: var(--accent);
}
#post_footer {
display: flex;
justify-content: space-between;
opacity: 0.5;
font-size: 14px;
}
#post_links {
display: flex;
list-style: none;
padding: 0;
font-weight: bold;
}
#post_links > li {
margin-right: 15px;
}
.post_subreddit {
font-weight: bold;
}
.post_title {
font-size: 16px; font-size: 16px;
line-height: 1.5;
margin-top: 10px;
}
.post_text {
padding: 15px;
display: flex;
flex-direction: column;
}
.post_right {
flex-grow: 1;
flex-shrink: 1;
justify-content: space-between;
}
.post_right > * {
margin: 5px;
}
.post_media {
max-width: 90%;
align-self: center;
margin-top: 15px;
}
.post_body {
opacity: 0.9;
font-weight: normal;
margin: 10px 5px;
}
#post_url {
color: var(--accent);
margin-top: 10px;
}
.post_thumbnail {
object-fit: cover;
width: auto;
flex-shrink: 0;
border-radius: 5px;
border: 1px solid var(--foreground);
max-width: 20%;
}
.post_flair {
background: var(--accent);
color: black;
padding: 5px;
border-radius: 5px;
font-size: 12px;
font-weight: bold;
} }
.nsfw { .nsfw {
@ -303,68 +436,14 @@ input[type="submit"]:hover { color: var(--accent); }
.stickied { .stickied {
--accent: #5cff85; --accent: #5cff85;
border: 1px solid #5cff85; border: 1px solid #5cff85;
padding: 5px;
}
.post_subreddit {
font-weight: bold;
}
.post_title {
font-size: 18px;
line-height: 1.5;
}
.post_right {
padding: 20px 25px;
flex-grow: 1;
flex-shrink: 1;
}
.post_right > * {
margin: 5px;
}
.post_media {
max-width: 90%;
align-self: center;
}
.post_body {
opacity: 0.9;
font-weight: normal;
margin: 10px 5px;
}
#post_url {
color: var(--accent);
}
.post_thumbnail {
object-fit: cover;
width: auto;
flex-shrink: 0;
padding: 10px;
border-radius: 15px;
max-width: 20%;
}
.post_flair {
background: var(--accent);
color: black;
padding: 5px;
border-radius: 5px;
font-size: 12px;
font-weight: bold;
} }
/* Comment */ /* Comment */
.comment { .comment {
margin-top: 15px; margin: 10px 0;
border-radius: 5px; border-radius: 5px;
display: flex; display: flex;
font-size: 15px;
} }
.comment_left, .comment_right { .comment_left, .comment_right {
@ -419,7 +498,7 @@ input[type="submit"]:hover { color: var(--accent); }
} }
.comment_data > * { .comment_data > * {
margin: 5px; margin-right: 5px;
} }
.comment_image { .comment_image {
@ -457,7 +536,7 @@ input[type="submit"]:hover { color: var(--accent); }
} }
.datetime { .datetime {
opacity: 0.75; opacity: 0.5;
} }
.line { .line {
@ -481,7 +560,6 @@ input[type="submit"]:hover { color: var(--accent); }
margin-top: 20px; margin-top: 20px;
} }
.md p { font-size: 15px; }
.md h1 { font-size: 22px; } .md h1 { font-size: 22px; }
.md h2 { font-size: 20px; } .md h2 { font-size: 20px; }
.md h3 { font-size: 18px; } .md h3 { font-size: 18px; }
@ -490,17 +568,17 @@ input[type="submit"]:hover { color: var(--accent); }
.md h6 { font-size: 12px; } .md h6 { font-size: 12px; }
.md blockquote { .md blockquote {
padding-left: 8px; padding-left: 6px;
margin: 4px 0 4px 8px; margin: 4px 0 4px 5px;
border-left: 4px solid var(--highlighted); border-left: 4px solid var(--highlighted);
} }
.md a { .md a {
text-decoration: underline;
color: var(--accent); color: var(--accent);
} }
.md li { margin: 10px 0; } .md li { margin: 10px 0; }
.toc_child { list-style: none; }
.md pre { .md pre {
background: var(--outside); background: var(--outside);
@ -510,6 +588,10 @@ input[type="submit"]:hover { color: var(--accent); }
box-shadow: var(--black-contrast); box-shadow: var(--black-contrast);
} }
.md table {
margin: 5px;
}
.md code { .md code {
font-family: monospace; font-family: monospace;
font-size: 14px; font-size: 14px;
@ -536,20 +618,23 @@ td, th {
flex-direction: column-reverse; flex-direction: column-reverse;
} }
.post_left { .post_header {
border-radius: 0 0 5px 5px; font-size: 14px;
} }
.post_right { .post_left {
padding: 20px; border-radius: 0 0 5px 5px;
flex-direction: row;
justify-content: center;
align-items: center;
}
.nsfw {
margin: 5px 0px 5px 10px;
} }
.post_score { .post_score {
margin-top: 0; margin: 5px 0;
}
.post_thumbnail {
max-width: initial;
} }
.replies > .comment { .replies > .comment {
@ -563,15 +648,24 @@ td, th {
} }
@media screen and (max-width: 800px) { @media screen and (max-width: 800px) {
main { flex-direction: column-reverse; } main {
nav { flex-direction: column; } flex-direction: column-reverse;
padding: 10px;
margin: 10px 0;
}
aside { nav {
margin: 20px 0 0 0; flex-direction: column;
padding: 10px;
}
aside, #subreddit, #user {
margin: 0;
max-width: 100%; max-width: 100%;
} }
#user, #sidebar { #user, #sidebar { margin: 20px 0; }
margin: 20px 0; #logo { margin: 5px auto; }
} #searchbox { width: 100%; }
#github { display: none; }
} }

View File

@ -4,21 +4,22 @@
{% block head %} {% block head %}
<title>{% block title %}Libreddit{% endblock %}</title> <title>{% block title %}Libreddit{% endblock %}</title>
<meta http-equiv="Referrer-Policy" content="no-referrer"> <meta http-equiv="Referrer-Policy" content="no-referrer">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; style-src 'self' 'unsafe-inline'; base-uri 'none'; form-action 'self'; <meta http-equiv="Content-Security-Policy" content="default-src 'self'; style-src 'self' 'unsafe-inline'; base-uri 'none'; form-action 'self';">
{% if cfg!(not(feature = "proxy")) %}img-src https://*; media-src https://*{% endif %}">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="description" content="View on Libreddit, an alternative private front-end to Reddit."> <meta name="description" content="View on Libreddit, an alternative private front-end to Reddit.">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/style.css"> <link rel="stylesheet" href="/style.css">
{% endblock %} {% endblock %}
</head> </head>
<body style="visibility: hidden;"> <body>
<!-- NAVIGATION BAR -->
<nav> <nav>
<a href="/"><span id="lib">lib</span>reddit. <span id="version">v{{ env!("CARGO_PKG_VERSION") }}</span></a> <a id="logo" href="/"><span id="lib">lib</span>reddit. <span id="version">v{{ env!("CARGO_PKG_VERSION") }}</span></a>
{% block search %}{% endblock %} {% block search %}{% endblock %}
<a id="github" href="https://github.com/spikecodes/libreddit">GITHUB</a> <a id="github" href="https://github.com/spikecodes/libreddit">GITHUB</a>
</nav> </nav>
<!-- MAIN CONTENT -->
{% block body %} {% block body %}
<main> <main>
{% block content %} {% block content %}

View File

@ -13,8 +13,8 @@
<meta name="author" content="u/{{ post.author }}"> <meta name="author" content="u/{{ post.author }}">
{% endblock %} {% endblock %}
<!-- OPEN COMMENT MACRO -->
{% macro comment(item) -%} {% macro comment(item) -%}
<div id="{{ item.id }}" class="comment"> <div id="{{ item.id }}" class="comment">
<div class="comment_left"> <div class="comment_left">
<p class="comment_score">{{ item.score }}</p> <p class="comment_score">{{ item.score }}</p>
@ -25,35 +25,45 @@
{% if item.flair.0 != "" %} {% if item.flair.0 != "" %}
<small class="author_flair">{{ item.flair.0 }}</small> <small class="author_flair">{{ item.flair.0 }}</small>
{% endif %} {% endif %}
&bull; <span class="datetime">{{ item.time }}</span> <span class="datetime">{{ item.time }}</span>
</summary> </summary>
<p class="comment_body">{{ item.body }}</p> <p class="comment_body">{{ item.body }}</p>
{%- endmacro %} {%- endmacro %}
<!-- CLOSE COMMENT MACRO -->
{% macro close() %}
</details></div>
{% endmacro %}
{% block content %} {% block content %}
<div id="column_one"> <div id="column_one">
<div class="post highlighted">
<!-- POST CONTENT -->
<div class="post highlighted panel">
<div class="post_left"> <div class="post_left">
<p class="post_score">{{ post.score }}</p> <p class="post_score">{{ post.score }}</p>
{% if post.flags.nsfw %}<div class="nsfw">NSFW</div>{% endif %} {% if post.flags.nsfw %}<div class="nsfw">NSFW</div>{% endif %}
</div> </div>
<div class="post_right"> <div class="post_right">
<p> <div class="post_text">
<b><a class="post_subreddit" href="/r/{{ post.community }}">r/{{ post.community }}</a></b> <p class="post_header">
&bull; <a class="post_subreddit" href="/r/{{ post.community }}">r/{{ post.community }}</a>
<span class="dot">&bull;</span>
<a class="post_author" href="/u/{{ post.author }}">u/{{ post.author }}</a> <a class="post_author" href="/u/{{ post.author }}">u/{{ post.author }}</a>
{% if post.author_flair.0 != "" %} {% if post.author_flair.0 != "" %}
<small class="author_flair">{{ post.author_flair.0 }}</small> <small class="author_flair">{{ post.author_flair.0 }}</small>
{% endif %} {% endif %}
<span class="dot">&bull;</span>
<span class="datetime">{{ post.time }}</span> <span class="datetime">{{ post.time }}</span>
</p> </p>
<a href="{{ post.url }}" class="post_title"> <a href="{{ post.permalink }}" class="post_title">
{{ post.title }} {{ post.title }}
{% if post.flair.0 != "" %} {% if post.flair.0 != "" %}
<small class="post_flair" style="color:{{ post.flair.2 }}; background:{{ post.flair.1 }}">{{ post.flair.0 }}</small> <small class="post_flair" style="color:{{ post.flair.2 }}; background:{{ post.flair.1 }}">{{ post.flair.0 }}</small>
{% endif %} {% endif %}
</a> </a>
<!-- POST MEDIA -->
{% if post.post_type == "image" %} {% if post.post_type == "image" %}
<img class="post_media" src="{{ post.media }}"/> <img class="post_media" src="{{ post.media }}"/>
{% else if post.post_type == "video" %} {% else if post.post_type == "video" %}
@ -61,38 +71,51 @@
{% else if post.post_type == "link" %} {% else if post.post_type == "link" %}
<a id="post_url" href="{{ post.media }}">{{ post.media }}</a> <a id="post_url" href="{{ post.media }}">{{ post.media }}</a>
{% endif %} {% endif %}
<!-- POST BODY -->
<div class="post_body">{{ post.body }}</div> <div class="post_body">{{ post.body }}</div>
<div id="post_footer">
<ul id="post_links">
<li><a href="/{{ post.id }}">permalink</a></li>
<li><a href="https://reddit.com/{{ post.id }}">reddit</a></li>
</ul>
<p>{{ post.upvote_ratio }}% Upvoted</p>
</div> </div>
</div> </div>
</div>
</div>
<!-- SORT FORM -->
<form id="sort"> <form id="sort">
<select name="sort"> <select name="sort">
{% call utils::options(sort, ["confidence", "top", "new", "controversial", "old"], "") %} {% call utils::options(sort, ["confidence", "top", "new", "controversial", "old"], "") %}
</select><input id="sort_submit" type="submit" value="&rarr;"> </select><input id="sort_submit" type="submit" value="&rarr;">
</form> </form>
<!-- COMMENTS -->
{% for c in comments -%} {% for c in comments -%}
<div class="thread"> <div class="thread">
<!-- EACH COMMENT -->
{% call comment(c) %} {% call comment(c) %}
<div class="replies"> <div class="replies">{% for reply1 in c.replies %}{% call comment(reply1) %}
{% for reply1 in c.replies %} <!-- FIRST-LEVEL REPLIES -->
{% call comment(reply1) %} <div class="replies">{% for reply2 in reply1.replies %}{% call comment(reply2) %}
<div class="replies"> <!-- SECOND-LEVEL REPLIES -->
{% for reply2 in reply1.replies %} <div class="replies">{% for reply3 in reply2.replies %}{% call comment(reply3) %}
{% call comment(reply2) %} <!-- THIRD-LEVEL REPLIES -->
<div class="replies">
{% for reply3 in reply2.replies %}
{% call comment(reply3) %}
{% if reply3.replies.len() > 0 %} {% if reply3.replies.len() > 0 %}
<a class="deeper_replies" href="{{ post.url }}{{ reply3.id }}">&rarr; More replies</a> <!-- LINK TO CONTINUE REPLIES -->
<a class="deeper_replies" href="{{ post.permalink }}{{ reply3.id }}">&rarr; More replies</a>
{% endif %} {% endif %}
</details></div> {% call close() %}
{% endfor %} {% endfor %}
</div></details></div> </div>{% call close() %}
{% endfor %} {% endfor %}
</div></details></div> </div>{% call close() %}
{% endfor %} {% endfor %}
</div></details></div> </div>{% call close() %}
</div> </div>
{%- endfor %} {%- endfor %}
</div> </div>
{% endblock %} {% endblock %}

View File

@ -1,47 +1,53 @@
{% extends "base.html" %} {% extends "base.html" %}
{% import "utils.html" as utils %} {% import "utils.html" as utils %}
{% block title %}Libreddit: search results - {{ params.q }}{% endblock %}
{% block content %} {% block content %}
<div id="column_one"> <div id="column_one">
<form id="search_sort"> <form id="search_sort">
<input id="search" type="text" name="q" placeholder="Search" value="{{ query }}"> <input id="search" type="text" name="q" placeholder="Search" value="{{ params.q }}">
{% if sub != "" %} {% if sub != "" %}
<div id="inside"> <div id="inside">
<input type="checkbox" name="restrict_sr" id="restrict_sr" checked="checked" data-com.bitwarden.browser.user-edited="yes"> <input type="checkbox" name="restrict_sr" id="restrict_sr" {% if params.restrict_sr != "" %}checked{% endif %}>
<label for="restrict_sr">in r/{{ sub }}</label> <label for="restrict_sr">in r/{{ sub }}</label>
</div> </div>
{% endif %} {% endif %}
<select id="sort_options" name="sort"> <select id="sort_options" name="sort">
{% call utils::options(sort.0, ["relevance", "hot", "top", "new", "comments"], "") %} {% call utils::options(params.sort, ["relevance", "hot", "top", "new", "comments"], "") %}
</select>{% if sort.0 != "new" %}<select id="timeframe" name="t"> </select>{% if params.sort != "new" %}<select id="timeframe" name="t">
{% call utils::options(sort.1, ["hour", "day", "week", "month", "year", "all"], "all") %} {% call utils::options(params.t, ["hour", "day", "week", "month", "year", "all"], "all") %}
</select>{% endif %}<input id="sort_submit" type="submit" value="&rarr;"> </select>{% endif %}<input id="sort_submit" type="submit" value="&rarr;">
</form> </form>
{% for post in posts %} {% for post in posts %}
{% if post.title != "Comment" %} {% if post.title != "Comment" %}
<div class="post"> <div class="post panel">
<div class="post_left"> <div class="post_left">
<p class="post_score">{{ post.score }}</p> <p class="post_score">{{ post.score }}</p>
{% if post.flags.nsfw %}<div class="nsfw">NSFW</div>{% endif %} {% if post.flags.nsfw %}<div class="nsfw">NSFW</div>{% endif %}
</div> </div>
<div class="post_right"> <div class="post_right">
<p> <div class="post_text">
<b><a class="post_subreddit" href="/r/{{ post.community }}">r/{{ post.community }}</a></b> <p class="post_header">
&bull; <a class="post_author" href="/u/{{ post.author }}">u/{{ post.author }}</a> <a class="post_subreddit" href="/r/{{ post.community }}">r/{{ post.community }}</a>
<span class="dot">&bull;</span>
<a class="post_author" href="/u/{{ post.author }}">u/{{ post.author }}</a>
{% if post.author_flair.0 != "" %} {% if post.author_flair.0 != "" %}
<small class="author_flair">{{ post.author_flair.0 }}</small> <small class="author_flair">{{ post.author_flair.0 }}</small>
{% endif %} {% endif %}
<span class="datetime" style="float: right;">{{ post.time }}</span> <span class="dot">&bull;</span>
<span class="datetime">{{ post.time }}</span>
</p> </p>
<p class="post_title"> <p class="post_title">
{% if post.flair.0 != "" %} {% if post.flair.0 != "" %}
<small class="post_flair" style="color:{{ post.flair.2 }}; background:{{ post.flair.1 }}">{{ post.flair.0 }}</small> <small class="post_flair" style="color:{{ post.flair.2 }}; background:{{ post.flair.1 }}">{{ post.flair.0 }}</small>
{% endif %} {% endif %}
<a href="{{ post.url }}">{{ post.title }}</a> <a href="{{ post.permalink }}">{{ post.title }}</a>
</p> </p>
</div> </div>
<img class="post_thumbnail" src="{{ post.media }}"> <img class="post_thumbnail" src="{{ post.media }}">
</div><br> </div>
</div>
{% else %} {% else %}
<div class="comment"> <div class="comment">
<div class="comment_left"> <div class="comment_left">
@ -50,22 +56,26 @@
</div> </div>
<details class="comment_right" open> <details class="comment_right" open>
<summary class="comment_data"> <summary class="comment_data">
<a class="comment_link" href="{{ post.url }}">COMMENT</a> <a class="comment_link" href="{{ post.permalink }}">COMMENT</a>
<span class="datetime">{{ post.time }}</span> <span class="datetime">{{ post.time }}</span>
</summary> </summary>
<p class="comment_body">{{ post.body }}</p> <p class="comment_body">{{ post.body }}</p>
</details> </details>
</div><br> </div>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
<footer> <footer>
{% if ends.0 != "" %} {% if params.before != "" %}
<a href="?sort={{ sort.0 }}{% if sort.0 == "top" %}&t={{ sort.1 }}{% endif %}&before={{ ends.0 }}">PREV</a> <a href="?q={{ params.q }}&restrict_sr={{ params.restrict_sr }}
&sort={{ params.sort }}&t={{ params.t }}
&before={{ params.before }}">PREV</a>
{% endif %} {% endif %}
{% if ends.1 != "" %} {% if params.after != "" %}
<a href="?sort={{ sort.0 }}{% if sort.0 == "top" %}&t={{ sort.1 }}{% endif %}&after={{ ends.1 }}">NEXT</a> <a href="?q={{ params.q }}&restrict_sr={{ params.restrict_sr }}
&sort={{ params.sort }}&t={{ params.t }}
&after={{ params.after }}">NEXT</a>
{% endif %} {% endif %}
</footer> </footer>
</div> </div>

18
templates/settings.html Normal file
View File

@ -0,0 +1,18 @@
{% extends "base.html" %}
{% import "utils.html" as utils %}
{% block title %}Libreddit Settings{% endblock %}
{% block search %}
{% call utils::search("".to_owned(), "", "") %}
{% endblock %}
{% block body %}
<main>
<form action="/settings/save" method="POST">
<label for="pref_nsfw">NSFW</label>
<input type="checkbox" name="pref_nsfw" id="pref_nsfw" {% if pref_nsfw == "on" %}checked{% endif %}>
<input id="sort_submit" type="submit" value="&rarr;">
</form>
</main>
{% endblock %}

View File

@ -1,7 +1,11 @@
{% extends "base.html" %} {% extends "base.html" %}
{% import "utils.html" as utils %} {% import "utils.html" as utils %}
{% block title %}r/{{ sub.name }}: {{ sub.description }}{% endblock %} {% block title %}
{% if sub.title != "" %}{{ sub.title }}
{% else if sub.name != "" %}{{ sub.name }}
{% else %}Libreddit{% endif %}
{% endblock %}
{% block search %} {% block search %}
{% call utils::search(["/r/", sub.name.as_str()].concat(), "") %} {% call utils::search(["/r/", sub.name.as_str()].concat(), "") %}
@ -13,40 +17,41 @@
<form id="sort"> <form id="sort">
<div id="sort_options"> <div id="sort_options">
{% if sub.name.is_empty() %} {% if sub.name.is_empty() %}
{% call utils::sort("", ["hot", "new", "top", "rising"], sort.0) %} {% call utils::sort("", ["hot", "new", "top", "rising", "controversial"], sort.0) %}
{% else %} {% else %}
{% call utils::sort(["/r/", sub.name.as_str()].concat(), ["hot", "new", "top", "rising"], sort.0) %} {% call utils::sort(["/r/", sub.name.as_str()].concat(), ["hot", "new", "top", "rising", "controversial"], sort.0) %}
{% endif %} {% endif %}
</div> </div>
{% if sort.0 == "top" %}<select id="timeframe" name="t"> {% if sort.0 == "top" || sort.0 == "controversial" %}<select id="timeframe" name="t">
{% call utils::options(sort.1, ["hour", "day", "week", "month", "year", "all"], "day") %} {% call utils::options(sort.1, ["hour", "day", "week", "month", "year", "all"], "day") %}
<input id="sort_submit" type="submit" value="&rarr;"> <input id="sort_submit" type="submit" value="&rarr;">
</select>{% endif %} </select>{% endif %}
</form> </form>
{% for post in posts %} {% for post in posts %}
<div class="post {% if post.flags.stickied %}stickied{% endif %}"> <div class="post {% if post.flags.stickied %}stickied{% endif %} panel">
<div class="post_left"> <div class="post_left">
<p class="post_score">{{ post.score }}</p> <p class="post_score">{{ post.score }}</p>
{% if post.flags.nsfw %}<div class="nsfw">NSFW</div>{% endif %} {% if post.flags.nsfw %}<div class="nsfw">NSFW</div>{% endif %}
</div> </div>
<div class="post_right"> <div class="post_right">
<p> <div class="post_text">
<b><a class="post_subreddit" href="/r/{{ post.community }}">r/{{ post.community }}</a></b> <p class="post_header">
&bull; <a class="post_author" href="/u/{{ post.author }}">u/{{ post.author }}</a> <a class="post_subreddit" href="/r/{{ post.community }}">r/{{ post.community }}</a>
{% if post.author_flair.0 != "" %} <span class="dot">&bull;</span>
<small class="author_flair">{{ post.author_flair.0 }}</small> <a class="post_author" href="/u/{{ post.author }}">u/{{ post.author }}</a>
{% endif %} <span class="dot">&bull;</span>
<span class="datetime">{{ post.time }}</span> <span class="datetime">{{ post.time }}</span>
</p> </p>
<p class="post_title"> <p class="post_title">
{% if post.flair.0 != "" %} {% if post.flair.0 != "" %}
<small class="post_flair" style="color:{{ post.flair.2 }}; background:{{ post.flair.1 }}">{{ post.flair.0 }}</small> <small class="post_flair" style="color:{{ post.flair.2 }}; background:{{ post.flair.1 }}">{{ post.flair.0 }}</small>
{% endif %} {% endif %}
<a href="{{ post.url }}">{{ post.title }}</a> <a href="{{ post.permalink }}">{{ post.title }}</a>
</p> </p>
</div> </div>
<img class="post_thumbnail" src="{{ post.media }}"> <img class="post_thumbnail" src="{{ post.media }}">
</div><br> </div>
</div>
{% endfor %} {% endfor %}
<footer> <footer>
@ -61,18 +66,27 @@
</div> </div>
{% if sub.name != "" %} {% if sub.name != "" %}
<aside> <aside>
<div id="subreddit"> <div class="panel" id="subreddit">
<img id="subreddit_icon" src="{{ sub.icon }}"> {% if sub.wiki %}
<p id="subreddit_name">r/{{ sub.name }}</p> <div id="top">
<p id="subreddit_description">{{ sub.description }}</p> <div>Posts</div>
<div id="subreddit_details"> <a href="/r/{{ sub.name }}/wiki/index">Wiki</a>
</div>
{% endif %}
<div id="sub_meta">
<img id="sub_icon" src="{{ sub.icon }}">
<p id="sub_title">{{ sub.title }}</p>
<p id="sub_name">r/{{ sub.name }}</p>
<p id="sub_description">{{ sub.description }}</p>
<div id="sub_details">
<label>Members</label> <label>Members</label>
<label>Active</label> <label>Active</label>
<div>{{ sub.members }}</div> <div>{{ sub.members }}</div>
<div>{{ sub.active }}</div> <div>{{ sub.active }}</div>
</div> </div>
</div> </div>
<details id="sidebar"> </div>
<details class="panel" id="sidebar">
<summary id="sidebar_label">Sidebar</summary> <summary id="sidebar_label">Sidebar</summary>
<div id="sidebar_contents">{{ sub.info }}</div> <div id="sidebar_contents">{{ sub.info }}</div>
</details> </details>

View File

@ -5,7 +5,7 @@
{% call utils::search("".to_owned(), "", "") %} {% call utils::search("".to_owned(), "", "") %}
{% endblock %} {% endblock %}
{% block title %}Libreddit: u/{{ user.name }}{% endblock %} {% block title %}{{ user.name.replace("u/", "") }} (u/{{ user.name }}) - Libreddit{% endblock %}
{% block body %} {% block body %}
<main style="max-width: 1000px;"> <main style="max-width: 1000px;">
<div id="column_one"> <div id="column_one">
@ -18,18 +18,19 @@
</form> </form>
{% for post in posts %} {% for post in posts %}
{% if post.title != "Comment" %} {% if post.title != "Comment" %}
<div class='post'> <div class="post panel">
<div class="post_left"> <div class="post_left">
<p class="post_score">{{ post.score }}</p> <p class="post_score">{{ post.score }}</p>
{% if post.flags.nsfw %}<div class="nsfw">NSFW</div>{% endif %} {% if post.flags.nsfw %}<div class="nsfw">NSFW</div>{% endif %}
</div> </div>
<div class="post_right"> <div class="post_right">
<p> <div class="post_text">
<b><a class="post_subreddit" href="/r/{{ post.community }}">r/{{ post.community }}</a></b> <p class="post_header">
&bull; <a class="post_author" href="/u/{{ post.author }}">u/{{ post.author }}</a> <a class="post_subreddit" href="/r/{{ post.community }}">r/{{ post.community }}</a>
{% if post.author_flair.0 != "" %} {% if post.author_flair.0 != "" %}
<small class="author_flair">{{ post.author_flair.0 }}</small> <small class="author_flair">{{ post.author_flair.0 }}</small>
{% endif %} {% endif %}
<span class="dot">&bull;</span>
<span class="datetime" style="float: right;">{{ post.time }}</span> <span class="datetime" style="float: right;">{{ post.time }}</span>
</p> </p>
<p class="post_title"> <p class="post_title">
@ -38,11 +39,12 @@
{% else %} {% else %}
<small class="post_flair" style="color:{{ post.flair.2 }}; background:{{ post.flair.1 }}">{{ post.flair.0 }}</small> <small class="post_flair" style="color:{{ post.flair.2 }}; background:{{ post.flair.1 }}">{{ post.flair.0 }}</small>
{% endif %} {% endif %}
<a href="{{ post.url }}">{{ post.title }}</a> <a href="{{ post.permalink }}">{{ post.title }}</a>
</p> </p>
</div> </div>
<img class="post_thumbnail" src="{{ post.media }}"> <img class="post_thumbnail" src="{{ post.media }}">
</div><br> </div>
</div>
{% else %} {% else %}
<div class="comment"> <div class="comment">
<div class="comment_left"> <div class="comment_left">
@ -51,12 +53,12 @@
</div> </div>
<details class="comment_right" open> <details class="comment_right" open>
<summary class="comment_data"> <summary class="comment_data">
<a class="comment_link" href="{{ post.url }}">COMMENT</a> <a class="comment_link" href="{{ post.permalink }}">COMMENT</a>
<span class="datetime">{{ post.time }}</span> <span class="datetime">{{ post.time }}</span>
</summary> </summary>
<p class="comment_body">{{ post.body }}</p> <p class="comment_body">{{ post.body }}</p>
</details> </details>
</div><br> </div>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
<footer> <footer>
@ -70,8 +72,9 @@
</footer> </footer>
</div> </div>
<aside> <aside>
<div id="user"> <div class="panel" id="user">
<img id="user_icon" src="{{ user.icon }}"> <img id="user_icon" src="{{ user.icon }}">
<p id="user_title">{{ user.title }}</p>
<p id="user_name">u/{{ user.name }}</p> <p id="user_name">u/{{ user.name }}</p>
<div id="user_description">{{ user.description }}</div> <div id="user_description">{{ user.description }}</div>
<div id="user_details"> <div id="user_details">

View File

@ -15,11 +15,11 @@
{%- endmacro %} {%- endmacro %}
{% macro search(root, search) -%} {% macro search(root, search) -%}
<form action="{% if root != "/r/" %}{{ root }}{% endif %}/search/" id="searchbox"> <form action="{% if root != "/r/" && !root.is_empty() %}{{ root }}{% endif %}/search/" id="searchbox">
<input id="search" type="text" name="q" placeholder="Search" value="{{ search }}"> <input id="search" type="text" name="q" placeholder="Search" value="{{ search }}">
{% if root != "/r/" %} {% if root != "/r/" && !root.is_empty() %}
<div id="inside"> <div id="inside">
<input type="checkbox" name="restrict_sr" id="restrict_sr" checked="checked" data-com.bitwarden.browser.user-edited="yes"> <input type="checkbox" name="restrict_sr" id="restrict_sr">
<label for="restrict_sr">in {{ root }}</label> <label for="restrict_sr">in {{ root }}</label>
</div> </div>
{% endif %} {% endif %}

25
templates/wiki.html Normal file
View File

@ -0,0 +1,25 @@
{% extends "base.html" %}
{% import "utils.html" as utils %}
{% block title %}
{% if sub != "" %}{{ page }} - {{ sub }}
{% else %}Libreddit{% endif %}
{% endblock %}
{% block search %}
{% call utils::search(["/r/", sub.as_str()].concat(), "") %}
{% endblock %}
{% block body %}
<main>
<div class="panel" id="column_one">
<div id="top">
<a href="/r/{{ sub }}">Posts</a>
<div>Wiki</div>
</div>
<div id="wiki">
{{ wiki }}
</div>
</div>
</main>
{% endblock %}