Compare commits

...

10 Commits

Author SHA1 Message Date
d50b6ca4b3 Add reddit.invak.id instance. Closes #157 2021-03-21 18:23:05 +00:00
4c66e75f6b Add HSTS command line flag 2021-03-20 22:10:31 -07:00
966e0ce921 Expand truncated numbers on mouseover. Close #156 2021-03-20 15:42:47 -07:00
ab886d1e67 Fix #155 2021-03-20 13:03:05 -07:00
dc7e087ed0 Truncate negative scores 2021-03-19 22:04:44 -07:00
0d6e18d97d Fill background of Apple Touch Icon 2021-03-18 21:36:39 -07:00
f872baa1fe Update 0.5.2 2021-03-18 21:35:14 -07:00
9b5176f7b9 Sub icons and truncated subscribers in search results 2021-03-18 21:32:54 -07:00
60c89197e5 Update "Built with" section of Readme 2021-03-18 16:53:03 +00:00
7d94876d90 Update screenshot 2021-03-18 16:47:10 +00:00
16 changed files with 116 additions and 122 deletions

View File

@ -3,7 +3,7 @@ name = "libreddit"
description = " Alternative private front-end to Reddit"
license = "AGPL-3.0"
repository = "https://github.com/spikecodes/libreddit"
version = "0.5.1"
version = "0.6.0"
authors = ["spikecodes <19519553+spikecodes@users.noreply.github.com>"]
edition = "2018"
@ -20,6 +20,6 @@ hyper = { version = "0.14.4", features = ["full"] }
hyper-rustls = "0.22.1"
route-recognizer = "0.3.0"
serde_json = "1.0.64"
tokio = { version = "1.3.0", features = ["full"] }
tokio = { version = "1.4.0", features = ["full"] }
time = "0.2.26"
url = "2.2.1"

View File

@ -2,7 +2,7 @@
> An alternative private front-end to Reddit
![screenshot](https://i.ibb.co/74gZ4pd/libreddit-rust.png)
![screenshot](https://i.ibb.co/QYbqTQt/libreddit-rust.png)
---
@ -48,6 +48,7 @@ Feel free to [open an issue](https://github.com/spikecodes/libreddit/issues/new)
| [libreddit.himiko.cloud](https://libreddit.himiko.cloud) | 🇫🇮 FI | |
| [libreddit.bcow.xyz](https://libreddit.bcow.xyz) | 🇺🇸 US | |
| [libreddit.40two.app](https://libreddit.40two.app) | 🇳🇱 NL | |
| [reddit.invak.id](https://reddit.invak.id) | 🇧🇬 BG | |
| [spjmllawtheisznfs7uryhxumin26ssv2draj7oope3ok3wuhy43eoyd.onion](http://spjmllawtheisznfs7uryhxumin26ssv2draj7oope3ok3wuhy43eoyd.onion) | 🇮🇳 IN | |
| [fwhhsbrbltmrct5hshrnqlqygqvcgmnek3cnka55zj4y7nuus5muwyyd.onion](http://fwhhsbrbltmrct5hshrnqlqygqvcgmnek3cnka55zj4y7nuus5muwyyd.onion) | 🇩🇪 DE | |
| [libreddit.himiko7xl2skojc6odi7hykl626gt4qki3vxdbv33u2u3af76d6k32ad.onion](http://libreddit.himiko7xl2skojc6odi7hykl626gt4qki3vxdbv33u2u3af76d6k32ad.onion) | 🇫🇮 FI | |
@ -64,9 +65,9 @@ Find Libreddit on 💬 [Matrix](https://matrix.to/#/#libreddit:kde.org), 🐋 [D
## Built with
- [Rust](https://www.rust-lang.org/) - Programming language
- [Tide](https://github.com/http-rs/tide) - Web server
- [Hyper](https://github.com/hyperium/hyper) - HTTP server and client
- [Askama](https://github.com/djc/askama) - Templating engine
- [Surf](https://github.com/http-rs/surf) - HTTP client
- [Rustls](https://github.com/ctz/rustls) - TLS library
## 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.

View File

@ -3,7 +3,6 @@ use futures_lite::{future::Boxed, FutureExt};
use hyper::{body::Buf, client, Body, Request, Response, Uri};
use serde_json::Value;
use std::{result::Result, str::FromStr};
// use async_recursion::async_recursion;
use crate::server::RequestExt;

View File

@ -73,11 +73,8 @@ async fn resource(body: &str, content_type: &str, cache: bool) -> Result<Respons
.unwrap_or_default();
if cache {
match HeaderValue::from_str("public, max-age=1209600, s-maxage=86400") {
Ok(val) => {
res.headers_mut().insert("Cache-Control", val);
}
Err(_) => (),
if let Ok(val) = HeaderValue::from_str("public, max-age=1209600, s-maxage=86400") {
res.headers_mut().insert("Cache-Control", val);
}
}
@ -114,11 +111,20 @@ async fn main() {
.help("Redirect all HTTP requests to HTTPS (no longer functional)")
.takes_value(false),
)
.arg(
Arg::with_name("hsts")
.short("H")
.long("hsts")
.value_name("EXPIRE_TIME")
.help("HSTS header to tell browsers that this site should only be accessed over HTTPS")
.default_value("604800")
.takes_value(true),
)
.get_matches();
let address = matches.value_of("address").unwrap_or("0.0.0.0");
let port = matches.value_of("port").unwrap_or("8080");
let _force_https = matches.is_present("redirect-https");
let hsts = matches.value_of("hsts");
let listener = format!("{}:{}", address, port);
@ -135,6 +141,12 @@ async fn main() {
"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';"
};
if let Some(expire_time) = hsts {
if let Ok(val) = HeaderValue::from_str(&format!("max-age={}", expire_time)) {
app.default_headers.insert("Strict-Transport-Security", val);
}
}
// Read static files
app.at("/style.css").get(|_| resource(include_str!("../static/style.css"), "text/css", false).boxed());
app
@ -175,6 +187,10 @@ async fn main() {
// Subreddit services
app.at("/r/:sub").get(|r| subreddit::community(r).boxed());
app
.at("/r/u_:name")
.get(|r| async move { Ok(redirect(format!("/user/{}", r.param("name").unwrap_or_default()))) }.boxed());
app.at("/r/:sub/subscribe").post(|r| subreddit::subscriptions(r).boxed());
app.at("/r/:sub/unsubscribe").post(|r| subreddit::subscriptions(r).boxed());
@ -202,7 +218,7 @@ async fn main() {
app.at("/").get(|r| subreddit::community(r).boxed());
// View Reddit wiki
app.at("/w").get(|_| async move { Ok(redirect("/wiki".to_string())) }.boxed());
app.at("/w").get(|_| async { Ok(redirect("/wiki".to_string())) }.boxed());
app
.at("/w/:page")
.get(|r| async move { Ok(redirect(format!("/wiki/{}", r.param("page").unwrap_or_default()))) }.boxed());
@ -215,18 +231,13 @@ async fn main() {
// Handle about pages
app.at("/about").get(|req| error(req, "About pages aren't added yet".to_string()).boxed());
app.at("/:id").get(|req: Request<Body>| {
async {
match req.param("id").as_deref() {
// Sort front page
Some("best") | Some("hot") | Some("new") | Some("top") | Some("rising") | Some("controversial") => subreddit::community(req).await,
// Short link for post
Some(id) if id.len() > 4 && id.len() < 7 => post::item(req).await,
// Error message for unknown pages
_ => error(req, "Nothing here".to_string()).await,
}
}
.boxed()
app.at("/:id").get(|req: Request<Body>| match req.param("id").as_deref() {
// Sort front page
Some("best") | Some("hot") | Some("new") | Some("top") | Some("rising") | Some("controversial") => subreddit::community(req).boxed(),
// Short link for post
Some(id) if id.len() > 4 && id.len() < 7 => post::item(req).boxed(),
// Error message for unknown pages
_ => error(req, "Nothing here".to_string()).boxed(),
});
// Default service in case no routes match

View File

@ -199,7 +199,7 @@ async fn parse_comments(json: &serde_json::Value, post_link: &str, post_author:
distinguished: val(&comment, "distinguished"),
},
score: if data["score_hidden"].as_bool().unwrap_or_default() {
"\u{2022}".to_string()
("\u{2022}".to_string(), "Hidden".to_string())
} else {
format_num(score)
},

View File

@ -1,5 +1,5 @@
// CRATES
use crate::utils::{cookie, error, param, template, val, Post, Preferences};
use crate::utils::{cookie, error, format_num, format_url, param, template, val, Post, Preferences};
use crate::{client::json, RequestExt};
use askama::Template;
use hyper::{Body, Request, Response};
@ -18,8 +18,9 @@ struct SearchParams {
struct Subreddit {
name: String,
url: String,
icon: String,
description: String,
subscribers: i64,
subscribers: (String, String),
}
#[derive(Template)]
@ -81,11 +82,22 @@ async fn search_subreddits(q: &str) -> Vec<Subreddit> {
// For each subreddit from subreddit list
Some(list) => list
.iter()
.map(|subreddit| Subreddit {
name: val(subreddit, "display_name_prefixed"),
url: val(subreddit, "url"),
description: val(subreddit, "public_description"),
subscribers: subreddit["data"]["subscribers"].as_f64().unwrap_or_default() as i64,
.map(|subreddit| {
// Fetch subreddit icon either from the community_icon or icon_img value
let community_icon: &str = subreddit["data"]["community_icon"].as_str().map_or("", |s| s.split('?').collect::<Vec<&str>>()[0]);
let icon = if community_icon.is_empty() {
val(&subreddit, "icon_img")
} else {
community_icon.to_string()
};
Subreddit {
name: val(subreddit, "display_name_prefixed"),
url: val(subreddit, "url"),
icon: format_url(&icon),
description: val(subreddit, "public_description"),
subscribers: format_num(subreddit["data"]["subscribers"].as_f64().unwrap_or_default() as i64),
}
})
.collect::<Vec<Subreddit>>(),
_ => Vec::new(),

View File

@ -28,9 +28,8 @@ macro_rules! headers(
{
let mut m = hyper::HeaderMap::new();
$(
match hyper::header::HeaderValue::from_str($value) {
Ok(val) => { m.insert($key, val); }
Err(_) => ()
if let Ok(val) = hyper::header::HeaderValue::from_str($value) {
m.insert($key, val);
}
)+
m
@ -96,11 +95,8 @@ impl ResponseExt for Response<Body> {
}
fn insert_cookie(&mut self, cookie: Cookie) {
match HeaderValue::from_str(&cookie.to_string()) {
Ok(val) => {
self.headers_mut().append("Set-Cookie", val);
}
Err(_) => (),
if let Ok(val) = HeaderValue::from_str(&cookie.to_string()) {
self.headers_mut().append("Set-Cookie", val);
}
}
@ -108,11 +104,8 @@ impl ResponseExt for Response<Body> {
let mut cookie = Cookie::named(name);
cookie.set_path("/");
cookie.set_max_age(Duration::second());
match HeaderValue::from_str(&cookie.to_string()) {
Ok(val) => {
self.headers_mut().append("Set-Cookie", val);
}
Err(_) => (),
if let Ok(val) = HeaderValue::from_str(&cookie.to_string()) {
self.headers_mut().append("Set-Cookie", val);
}
}
}

View File

@ -181,7 +181,7 @@ pub struct Post {
pub body: String,
pub author: Author,
pub permalink: String,
pub score: String,
pub score: (String, String),
pub upvote_ratio: i64,
pub post_type: String,
pub flair: Flair,
@ -191,7 +191,7 @@ pub struct Post {
pub domain: String,
pub rel_time: String,
pub created: String,
pub comments: String,
pub comments: (String, String),
pub gallery: Vec<GalleryMedia>,
}
@ -251,7 +251,7 @@ impl Post {
distinguished: val(post, "distinguished"),
},
score: if data["hide_score"].as_bool().unwrap_or_default() {
"\u{2022}".to_string()
("\u{2022}".to_string(), "Hidden".to_string())
} else {
format_num(score)
},
@ -307,7 +307,7 @@ pub struct Comment {
pub post_author: String,
pub body: String,
pub author: Author,
pub score: String,
pub score: (String, String),
pub rel_time: String,
pub created: String,
pub edited: (String, String),
@ -342,8 +342,8 @@ pub struct Subreddit {
pub description: String,
pub info: String,
pub icon: String,
pub members: String,
pub active: String,
pub members: (String, String),
pub active: (String, String),
pub wiki: bool,
}
@ -414,8 +414,8 @@ pub fn format_url(url: &str) -> String {
Regex::new(regex)
.map(|re| match re.captures(url) {
Some(caps) => match segments {
1 => [format, &caps[1], "/"].join(""),
2 => [format, &caps[1], "/", &caps[2], "/"].join(""),
1 => [format, &caps[1]].join(""),
2 => [format, &caps[1], "/", &caps[2]].join(""),
_ => String::new(),
},
None => String::new(),
@ -443,21 +443,23 @@ pub fn format_url(url: &str) -> String {
// Rewrite Reddit links to Libreddit in body of text
pub fn rewrite_urls(text: &str) -> String {
match Regex::new(r#"href="(https|http|)://(www.|old.|np.|)(reddit).(com)/"#) {
match Regex::new(r#"href="(https|http|)://(www.|old.|np.|amp.|)(reddit).(com)/"#) {
Ok(re) => re.replace_all(text, r#"href="/"#).to_string(),
Err(_) => String::new(),
}
}
// Append `m` and `k` for millions and thousands respectively
pub fn format_num(num: i64) -> String {
if num >= 1_000_000 {
pub fn format_num(num: i64) -> (String, String) {
let truncated = if num >= 1_000_000 || num <= -1_000_000 {
format!("{}m", num / 1_000_000)
} else if num >= 1000 {
} else if num >= 1000 || num <= -1000 {
format!("{}k", num / 1_000)
} else {
num.to_string()
}
};
(truncated, num.to_string())
}
// Parse a relative and absolute time from a UNIX timestamp
@ -539,49 +541,3 @@ pub async fn error(req: Request<Body>, msg: String) -> Result<Response<Body>, St
Ok(Response::builder().status(404).header("content-type", "text/html").body(body.into()).unwrap_or_default())
}
// #[async_recursion]
// async fn connect(path: String) -> io::Result<String> {
// // Construct an HTTP request body
// let req = format!(
// "GET {} HTTP/1.1\r\nHost: www.reddit.com\r\nAccept: */*\r\nConnection: close\r\nUser-Agent: {}\r\n\r\n",
// path, user_agent
// );
// // Open a TCP connection
// let tcp_stream = TcpStream::connect("www.reddit.com:443").await?;
// // Initialize TLS connector for requests
// let connector = TlsConnector::default();
// // Use the connector to start the handshake process
// let mut tls_stream = connector.connect("www.reddit.com", tcp_stream).await?;
// // Write the crafted HTTP request to the stream
// tls_stream.write_all(req.as_bytes()).await?;
// // And read the response
// let mut writer = Vec::new();
// io::copy(&mut tls_stream, &mut writer).await?;
// let response = String::from_utf8_lossy(&writer).to_string();
// let split = response.split("\r\n\r\n").collect::<Vec<&str>>();
// let headers = split[0].split("\r\n").collect::<Vec<&str>>();
// let status: i16 = headers[0].split(' ').collect::<Vec<&str>>()[1].parse().unwrap_or(200);
// let body = split[1].to_string();
// if (300..400).contains(&status) {
// let location = headers
// .iter()
// .find(|header| header.starts_with("location:"))
// .map(|f| f.to_owned())
// .unwrap_or_default()
// .split(": ")
// .collect::<Vec<&str>>()[1];
// connect(location.replace("https://www.reddit.com", "")).await
// } else {
// Ok(body)
// }
// }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 8.0 KiB

View File

@ -522,7 +522,26 @@ button.submit:hover > svg { stroke: var(--accent); }
.search_subreddit {
padding: 16px 20px;
display: block;
display: flex;
}
.search_subreddit_left {
display: flex;
align-items: center;
}
.search_subreddit_left:not(:empty) {
margin-right: 10px;
}
.search_subreddit_left img {
width: 35px;
height: 35px;
border-radius: 100%;
}
.search_subreddit_right {
overflow: auto;
}
a.search_subreddit:hover {
@ -1200,9 +1219,9 @@ td, th {
/* .thread { margin-left: -5px; } */
.comment_right { padding: 5px 0 10px 2px; }
.comment_author { margin-left: 5px; }
.comment_author { margin-left: 12px; }
.comment_data { margin-left: 12px; }
.comment_data::marker { font-size: 22px; }
.comment_data::marker { font-size: 25px; }
.created { width: 100%; }
.comment_score {

View File

@ -5,7 +5,7 @@
{% else if kind == "t1" %}
<div id="{{ id }}" class="comment">
<div class="comment_left">
<p class="comment_score">{{ score }}</p>
<p class="comment_score" title="{{ score.1 }}">{{ score.0 }}</p>
<div class="line"></div>
</div>
<details class="comment_right" open>

View File

@ -89,7 +89,7 @@
<!-- POST BODY -->
<div class="post_body">{{ post.body }}</div>
<div class="post_score">{{ post.score }}<span class="label"> Upvotes</span></div>
<div class="post_score" title="{{ post.score.1 }}">{{ post.score.0 }}<span class="label"> Upvotes</span></div>
<div class="post_footer">
<ul id="post_links">
<li><a href="/{{ post.id }}">permalink</a></li>

View File

@ -34,12 +34,15 @@
<div id="search_subreddits">
{% for subreddit in subreddits %}
<a href="{{ subreddit.url }}" class="search_subreddit">
<p class="search_subreddit_header">
<span class="search_subreddit_name">{{ subreddit.name }}</span>
<span class="dot">&bull;</span>
<span class="search_subreddit_members">{{ subreddit.subscribers }} Members</span>
</p>
<p class="search_subreddit_description">{{ subreddit.description }}</p>
<div class="search_subreddit_left">{% if subreddit.icon != "" %}<img src="{{ subreddit.icon }}" alt="r/{{ subreddit.name }} icon">{% endif %}</div>
<div class="search_subreddit_right">
<p class="search_subreddit_header">
<span class="search_subreddit_name">{{ subreddit.name }}</span>
<span class="dot">&bull;</span>
<span class="search_subreddit_members" title="{{ subreddit.subscribers.1 }} Members">{{ subreddit.subscribers.0 }} Members</span>
</p>
<p class="search_subreddit_description">{{ subreddit.description }}</p>
</div>
</a>
{% endfor %}
</div>
@ -52,7 +55,7 @@
{% else %}
<div class="comment">
<div class="comment_left">
<p class="comment_score">{{ post.score }}</p>
<p class="comment_score" title="{{ post.score.1 }}">{{ post.score.0 }}</p>
<div class="line"></div>
</div>
<details class="comment_right" open>

View File

@ -81,8 +81,8 @@
<div id="sub_details">
<label>Members</label>
<label>Active</label>
<div>{{ sub.members }}</div>
<div>{{ sub.active }}</div>
<div title="{{ sub.members.1 }}">{{ sub.members.0 }}</div>
<div title="{{ sub.active.1 }}">{{ sub.active.0 }}</div>
</div>
<div id="sub_subscription">
{% if prefs.subscriptions.contains(sub.name) %}

View File

@ -37,7 +37,7 @@
{% else %}
<div class="comment">
<div class="comment_left">
<p class="comment_score">{{ post.score }}</p>
<p class="comment_score" title="{{ post.score.1 }}">{{ post.score.0 }}</p>
<div class="line"></div>
</div>
<details class="comment_right" open>

View File

@ -108,9 +108,9 @@
</a>
{% endif %}
<div class="post_score">{{ post.score }}<span class="label"> Upvotes</span></div>
<div class="post_score" title="{{ post.score.1 }}">{{ post.score.0 }}<span class="label"> Upvotes</span></div>
<div class="post_footer">
<a href="{{ post.permalink }}" class="post_comments">{{ post.comments }} comments</a>
<a href="{{ post.permalink }}" class="post_comments" title="{{ post.comments.1 }} comments">{{ post.comments.0 }} comments</a>
</div>
</div>
{%- endmacro %}