Compare commits
18 Commits
Author | SHA1 | Date | |
---|---|---|---|
66ac72beab | |||
14f9ac4ca7 | |||
6a7f725c12 | |||
2533e8cef5 | |||
772d20615b | |||
0bb1677520 | |||
da4883db29 | |||
d50b6ca4b3 | |||
4c66e75f6b | |||
966e0ce921 | |||
ab886d1e67 | |||
dc7e087ed0 | |||
0d6e18d97d | |||
f872baa1fe | |||
9b5176f7b9 | |||
60c89197e5 | |||
7d94876d90 | |||
467342edf4 |
22
.github/workflows/rust.yml
vendored
22
.github/workflows/rust.yml
vendored
@ -21,9 +21,29 @@ jobs:
|
||||
|
||||
- name: Build
|
||||
run: cargo build --release
|
||||
|
||||
|
||||
- uses: actions/upload-artifact@v2.2.1
|
||||
name: Upload a Build Artifact
|
||||
with:
|
||||
name: libreddit
|
||||
path: target/release/libreddit
|
||||
|
||||
- name: Versions
|
||||
id: version
|
||||
run: |
|
||||
echo "::set-output name=version::$(cargo metadata --format-version 1 --no-deps | jq .packages[0].version -r | sed 's/^/v/')"
|
||||
echo "::set-output name=tag::${GITHUB_REF#refs/*/}"
|
||||
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
tag_name: ${{ steps.version.outputs.version }}
|
||||
name: ${{ steps.version.outputs.version }} - NAME
|
||||
draft: true
|
||||
files: target/release/libreddit
|
||||
body: |
|
||||
- CHANGES
|
||||
|
||||
See full list of changes [here](https://github.com/spikecodes/libreddit/compare/${{ steps.version.outputs.tag }}...${{ steps.version.outputs.version }}).
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||
|
@ -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.0"
|
||||
version = "0.8.1"
|
||||
authors = ["spikecodes <19519553+spikecodes@users.noreply.github.com>"]
|
||||
edition = "2018"
|
||||
|
||||
@ -13,13 +13,13 @@ async-recursion = "0.3.2"
|
||||
cached = "0.23.0"
|
||||
clap = { version = "2.33.3", default-features = false }
|
||||
regex = "1.4.5"
|
||||
serde = { version = "1.0.124", features = ["derive"] }
|
||||
serde = { version = "1.0.125", features = ["derive"] }
|
||||
cookie = "0.15.0"
|
||||
futures-lite = "1.11.3"
|
||||
hyper = { version = "0.14.4", features = ["full"] }
|
||||
hyper = { version = "0.14.5", 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"
|
||||
|
22
README.md
22
README.md
@ -2,7 +2,7 @@
|
||||
|
||||
> An alternative private front-end to Reddit
|
||||
|
||||

|
||||

|
||||
|
||||
---
|
||||
|
||||
@ -21,20 +21,6 @@
|
||||
|
||||
---
|
||||
|
||||
## Jump to...
|
||||
- [About](#about)
|
||||
- [Teddit Comparison](#how-does-it-compare-to-teddit)
|
||||
- [Comparison](#comparison)
|
||||
- [Installation](#installation)
|
||||
- [Cargo](#1-cargo)
|
||||
- [Docker](#2-docker)
|
||||
- [AUR](#3-aur)
|
||||
- [GitHub Releases](#4-github-releases)
|
||||
- [Replit](#5-replit)
|
||||
- [Deployment](#deployment)
|
||||
|
||||
---
|
||||
|
||||
# Instances
|
||||
|
||||
Feel free to [open an issue](https://github.com/spikecodes/libreddit/issues/new) to have your [selfhosted instance](#deployment) listed here!
|
||||
@ -48,6 +34,8 @@ 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 | |
|
||||
| [reddit.phii.me](https://reddit.phii.me) | 🇺🇸 US | |
|
||||
| [spjmllawtheisznfs7uryhxumin26ssv2draj7oope3ok3wuhy43eoyd.onion](http://spjmllawtheisznfs7uryhxumin26ssv2draj7oope3ok3wuhy43eoyd.onion) | 🇮🇳 IN | |
|
||||
| [fwhhsbrbltmrct5hshrnqlqygqvcgmnek3cnka55zj4y7nuus5muwyyd.onion](http://fwhhsbrbltmrct5hshrnqlqygqvcgmnek3cnka55zj4y7nuus5muwyyd.onion) | 🇩🇪 DE | |
|
||||
| [libreddit.himiko7xl2skojc6odi7hykl626gt4qki3vxdbv33u2u3af76d6k32ad.onion](http://libreddit.himiko7xl2skojc6odi7hykl626gt4qki3vxdbv33u2u3af76d6k32ad.onion) | 🇫🇮 FI | |
|
||||
@ -64,9 +52,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.
|
||||
|
@ -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;
|
||||
|
||||
@ -135,6 +134,6 @@ pub async fn json(path: String) -> Result<Value, String> {
|
||||
Err(e) => err("Failed receiving body from Reddit", e.to_string()),
|
||||
}
|
||||
}
|
||||
Err(e) => err("Couldn't send request to Reddit", e.to_string()),
|
||||
Err(e) => err("Couldn't send request to Reddit", e),
|
||||
}
|
||||
}
|
||||
|
54
src/main.rs
54
src/main.rs
@ -6,7 +6,8 @@
|
||||
clippy::match_wildcard_for_single_variants,
|
||||
clippy::cast_possible_truncation,
|
||||
clippy::similar_names,
|
||||
clippy::cast_possible_wrap
|
||||
clippy::cast_possible_wrap,
|
||||
clippy::find_map
|
||||
)]
|
||||
|
||||
// Reference local files
|
||||
@ -73,11 +74,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 +112,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 +142,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 +188,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());
|
||||
|
||||
@ -193,6 +210,8 @@ async fn main() {
|
||||
app.at("/r/:sub/wiki").get(|r| subreddit::wiki(r).boxed());
|
||||
app.at("/r/:sub/wiki/:page").get(|r| subreddit::wiki(r).boxed());
|
||||
|
||||
app.at("/r/:sub/about/sidebar").get(|r| subreddit::sidebar(r).boxed());
|
||||
|
||||
app.at("/r/:sub/:sort").get(|r| subreddit::community(r).boxed());
|
||||
|
||||
// Comments handler
|
||||
@ -202,7 +221,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 +234,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") {
|
||||
// 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
|
||||
|
@ -40,7 +40,7 @@ pub async fn item(req: Request<Body>) -> Result<Response<Body>, String> {
|
||||
#[cfg(debug_assertions)]
|
||||
dbg!(req.param("id").unwrap_or_default());
|
||||
|
||||
let single_thread = &req.param("comment_id").is_some();
|
||||
let single_thread = req.param("comment_id").is_some();
|
||||
let highlighted_comment = &req.param("comment_id").unwrap_or_default();
|
||||
|
||||
// Send a request to the url, receive JSON in response
|
||||
@ -57,7 +57,7 @@ pub async fn item(req: Request<Body>) -> Result<Response<Body>, String> {
|
||||
post,
|
||||
sort,
|
||||
prefs: Preferences::new(req),
|
||||
single_thread: *single_thread,
|
||||
single_thread,
|
||||
})
|
||||
}
|
||||
// If the Reddit API returns an error, exit and send error page to user
|
||||
@ -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)
|
||||
},
|
||||
|
@ -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(),
|
||||
|
@ -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
|
||||
@ -62,7 +61,7 @@ impl RequestExt for Request<Body> {
|
||||
}
|
||||
|
||||
fn param(&self, name: &str) -> Option<String> {
|
||||
self.params().find(name).map(|s| s.to_owned())
|
||||
self.params().find(name).map(std::borrow::ToOwned::to_owned)
|
||||
}
|
||||
|
||||
fn set_params(&mut self, params: Params) -> Option<Params> {
|
||||
@ -73,14 +72,14 @@ impl RequestExt for Request<Body> {
|
||||
let mut cookies = Vec::new();
|
||||
if let Some(header) = self.headers().get("Cookie") {
|
||||
for cookie in header.to_str().unwrap_or_default().split("; ") {
|
||||
cookies.push(Cookie::parse(cookie).unwrap_or(Cookie::named("")));
|
||||
cookies.push(Cookie::parse(cookie).unwrap_or_else(|_| Cookie::named("")));
|
||||
}
|
||||
}
|
||||
cookies
|
||||
}
|
||||
|
||||
fn cookie(&self, name: &str) -> Option<Cookie> {
|
||||
self.cookies().iter().find(|c| c.name() == name).map(|c| c.to_owned())
|
||||
self.cookies().iter().find(|c| c.name() == name).map(std::borrow::ToOwned::to_owned)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -16,19 +16,6 @@ struct SettingsTemplate {
|
||||
prefs: Preferences,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Default, Debug)]
|
||||
#[serde(default)]
|
||||
pub struct Form {
|
||||
theme: Option<String>,
|
||||
front_page: Option<String>,
|
||||
layout: Option<String>,
|
||||
wide: Option<String>,
|
||||
comment_sort: Option<String>,
|
||||
show_nsfw: Option<String>,
|
||||
redirect: Option<String>,
|
||||
subscriptions: Option<String>,
|
||||
}
|
||||
|
||||
// FUNCTIONS
|
||||
|
||||
// Retrieve cookies from request "Cookie" header
|
||||
@ -63,7 +50,7 @@ pub async fn set(req: Request<Body>) -> Result<Response<Body>, String> {
|
||||
|
||||
let mut res = redirect("/settings".to_string());
|
||||
|
||||
let names = vec!["theme", "front_page", "layout", "wide", "comment_sort", "show_nsfw", "subscriptions"];
|
||||
let names = vec!["theme", "front_page", "layout", "wide", "comment_sort", "post_sort", "show_nsfw", "subscriptions"];
|
||||
|
||||
for name in names {
|
||||
match form.get(name) {
|
||||
@ -98,7 +85,7 @@ pub async fn restore(req: Request<Body>) -> Result<Response<Body>, String> {
|
||||
|
||||
let form = url::form_urlencoded::parse(query).collect::<HashMap<_, _>>();
|
||||
|
||||
let names = vec!["theme", "front_page", "layout", "wide", "comment_sort", "show_nsfw", "subscriptions"];
|
||||
let names = vec!["theme", "front_page", "layout", "wide", "comment_sort", "post_sort", "show_nsfw", "subscriptions"];
|
||||
|
||||
let path = match form.get("redirect") {
|
||||
Some(value) => format!("/{}/", value),
|
||||
|
@ -32,17 +32,21 @@ pub async fn community(req: Request<Body>) -> Result<Response<Body>, String> {
|
||||
// Build Reddit API path
|
||||
let subscribed = cookie(&req, "subscriptions");
|
||||
let front_page = cookie(&req, "front_page");
|
||||
let sort = req.param("sort").unwrap_or_else(|| req.param("id").unwrap_or("hot".to_string()));
|
||||
let post_sort = req.cookie("post_sort").map_or_else(|| "hot".to_string(), |c| c.value().to_string());
|
||||
let sort = req.param("sort").unwrap_or_else(|| req.param("id").unwrap_or(post_sort));
|
||||
|
||||
let sub = req.param("sub").map(String::from).unwrap_or(if front_page == "default" || front_page.is_empty() {
|
||||
if subscribed.is_empty() {
|
||||
"popular".to_string()
|
||||
let sub = req.param("sub").map_or(
|
||||
if front_page == "default" || front_page.is_empty() {
|
||||
if subscribed.is_empty() {
|
||||
"popular".to_string()
|
||||
} else {
|
||||
subscribed.to_owned()
|
||||
}
|
||||
} else {
|
||||
subscribed.to_owned()
|
||||
}
|
||||
} else {
|
||||
front_page.to_owned()
|
||||
});
|
||||
front_page.to_owned()
|
||||
},
|
||||
String::from,
|
||||
);
|
||||
|
||||
let path = format!("/r/{}/{}.json?{}&raw_json=1", sub, sort, req.uri().query().unwrap_or_default());
|
||||
|
||||
@ -88,7 +92,7 @@ pub async fn community(req: Request<Body>) -> Result<Response<Body>, String> {
|
||||
|
||||
// Sub or unsub by setting subscription cookie using response "Set-Cookie" header
|
||||
pub async fn subscriptions(req: Request<Body>) -> Result<Response<Body>, String> {
|
||||
let sub = req.param("sub").unwrap_or_default().to_string();
|
||||
let sub = req.param("sub").unwrap_or_default();
|
||||
let query = req.uri().query().unwrap_or_default().to_string();
|
||||
let action: Vec<String> = req.uri().path().split('/').map(String::from).collect();
|
||||
|
||||
@ -136,8 +140,8 @@ pub async fn subscriptions(req: Request<Body>) -> Result<Response<Body>, String>
|
||||
}
|
||||
|
||||
pub async fn wiki(req: Request<Body>) -> Result<Response<Body>, String> {
|
||||
let sub = req.param("sub").unwrap_or("reddit.com".to_string());
|
||||
let page = req.param("page").unwrap_or("index".to_string());
|
||||
let sub = req.param("sub").unwrap_or_else(|| "reddit.com".to_string());
|
||||
let page = req.param("page").unwrap_or_else(|| "index".to_string());
|
||||
let path: String = format!("/r/{}/wiki/{}.json?raw_json=1", sub, page);
|
||||
|
||||
match json(path).await {
|
||||
@ -151,6 +155,25 @@ pub async fn wiki(req: Request<Body>) -> Result<Response<Body>, String> {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn sidebar(req: Request<Body>) -> Result<Response<Body>, String> {
|
||||
let sub = req.param("sub").unwrap_or_else(|| "reddit.com".to_string());
|
||||
|
||||
// Build the Reddit JSON API url
|
||||
let path: String = format!("/r/{}/about.json?raw_json=1", sub);
|
||||
|
||||
// Send a request to the url
|
||||
match json(path).await {
|
||||
// If success, receive JSON in response
|
||||
Ok(response) => template(WikiTemplate {
|
||||
sub,
|
||||
wiki: rewrite_urls(&val(&response, "description_html").replace("\\", "")),
|
||||
page: "Sidebar".to_string(),
|
||||
prefs: Preferences::new(req),
|
||||
}),
|
||||
Err(msg) => error(req, msg).await,
|
||||
}
|
||||
}
|
||||
|
||||
// SUBREDDIT
|
||||
async fn subreddit(sub: &str) -> Result<Subreddit, String> {
|
||||
// Build the Reddit JSON API url
|
||||
|
@ -23,7 +23,7 @@ pub async fn profile(req: Request<Body>) -> Result<Response<Body>, String> {
|
||||
// Build the Reddit JSON API path
|
||||
let path = format!(
|
||||
"/user/{}.json?{}&raw_json=1",
|
||||
req.param("name").unwrap_or("reddit".to_string()),
|
||||
req.param("name").unwrap_or_else(|| "reddit".to_string()),
|
||||
req.uri().query().unwrap_or_default()
|
||||
);
|
||||
|
||||
|
76
src/utils.rs
76
src/utils.rs
@ -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,
|
||||
}
|
||||
|
||||
@ -365,6 +365,7 @@ pub struct Preferences {
|
||||
pub wide: String,
|
||||
pub show_nsfw: String,
|
||||
pub comment_sort: String,
|
||||
pub post_sort: String,
|
||||
pub subscriptions: Vec<String>,
|
||||
}
|
||||
|
||||
@ -378,6 +379,7 @@ impl Preferences {
|
||||
wide: cookie(&req, "wide"),
|
||||
show_nsfw: cookie(&req, "show_nsfw"),
|
||||
comment_sort: cookie(&req, "comment_sort"),
|
||||
post_sort: cookie(&req, "post_sort"),
|
||||
subscriptions: cookie(&req, "subscriptions").split('+').map(String::from).filter(|s| !s.is_empty()).collect(),
|
||||
}
|
||||
}
|
||||
@ -414,8 +416,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 +445,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 +543,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 |
@ -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 {
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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">•</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">•</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>
|
||||
|
@ -36,6 +36,12 @@
|
||||
<input type="checkbox" name="wide" {% if prefs.wide == "on" %}checked{% endif %}>
|
||||
</div>
|
||||
<p>Content</p>
|
||||
<div id="post_sort">
|
||||
<label for="post_sort" title="Applies only to subreddit feeds">Default subreddit post sort:</label>
|
||||
<select name="post_sort">
|
||||
{% call utils::options(prefs.post_sort, ["hot", "new", "top", "rising", "controversial"], "hot") %}
|
||||
</select>
|
||||
</div>
|
||||
<div id="comment_sort">
|
||||
<label for="comment_sort">Default comment sort:</label>
|
||||
<select name="comment_sort">
|
||||
|
@ -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) %}
|
||||
|
@ -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>
|
||||
|
@ -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 %}
|
||||
|
Reference in New Issue
Block a user