Compare commits
19 Commits
Author | SHA1 | Date | |
---|---|---|---|
04310c58e0 | |||
6def67ddfe | |||
c33f7947b0 | |||
98d10d6596 | |||
863b512718 | |||
d6971bb9a3 | |||
fc98ca9af9 | |||
f33af75267 | |||
759c9fc66b | |||
9d78266494 | |||
9a6430656d | |||
8f157c0b40 | |||
3902a36ea3 | |||
9b049be627 | |||
3942388b25 | |||
74917c672f | |||
0b92868bfe | |||
b218ec6065 | |||
8a488af594 |
26
.github/workflows/rust.yml
vendored
Normal file
26
.github/workflows/rust.yml
vendored
Normal file
@ -0,0 +1,26 @@
|
||||
name: Rust
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- 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
|
983
Cargo.lock
generated
983
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
11
Cargo.toml
11
Cargo.toml
@ -3,13 +3,18 @@ name = "libreddit"
|
||||
description = " Alternative private front-end to Reddit"
|
||||
license = "AGPL-3.0"
|
||||
repository = "https://github.com/spikecodes/libreddit"
|
||||
version = "0.1.6"
|
||||
version = "0.1.10"
|
||||
authors = ["spikecodes <19519553+spikecodes@users.noreply.github.com>"]
|
||||
edition = "2018"
|
||||
|
||||
[features]
|
||||
default = ["proxy"]
|
||||
proxy = ["actix-web/rustls", "base64"]
|
||||
|
||||
[dependencies]
|
||||
actix-web = { version = "3.2.0", features = ["rustls"] }
|
||||
surf = "2.1.0"
|
||||
base64 = { version = "0.13.0", optional = true }
|
||||
actix-web = "3.2.0"
|
||||
reqwest = { version = "0.10", default_features = false, features = ["rustls-tls"] }
|
||||
askama = "0.8.0"
|
||||
serde = "1.0.117"
|
||||
serde_json = "1.0"
|
||||
|
95
README.md
95
README.md
@ -14,61 +14,38 @@ Think Invidious but for Reddit. Watch your cat videos without being watched.
|
||||
|
||||
## Screenshot
|
||||
|
||||

|
||||

|
||||
|
||||
## Status
|
||||
## About
|
||||
|
||||
- [x] Hosting
|
||||
- [x] Instances
|
||||
- [x] Clearnet instance
|
||||
- [ ] .onion instance
|
||||
- [x] Cargo deployment
|
||||
- [x] Docker deployment
|
||||
- [x] Subreddits
|
||||
- [x] Title
|
||||
- [x] Description
|
||||
- [x] Posts
|
||||
- [x] Post sorting
|
||||
- [x] Posts
|
||||
- [x] Flairs
|
||||
- [x] Comments
|
||||
- [x] Comment sorting
|
||||
- [ ] Nested comments
|
||||
- [x] UTC post date
|
||||
- [x] Image thumbnails
|
||||
- [x] Embedded images
|
||||
- [x] Proxied images
|
||||
- [x] Reddit-hosted video
|
||||
- [x] Proxied video
|
||||
- [x] Users
|
||||
- [x] Username
|
||||
- [x] Karma
|
||||
- [x] Description
|
||||
- [x] Post history
|
||||
- [x] Comment history
|
||||
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.
|
||||
|
||||
- [ ] Search
|
||||
- [ ] Post aggregating
|
||||
- [ ] Comment aggregating
|
||||
- [ ] Result sorting
|
||||
Libreddit currently implements most of Reddit's functionalities but still lacks a few features that are being worked on below.
|
||||
|
||||
### In Progress
|
||||
- Nested comments
|
||||
- User flairs
|
||||
- Searching
|
||||
|
||||
### How does it compare to Teddit?
|
||||
|
||||
Teddit is another awesome open source project designed to provide an alternative frontend to Reddit. There is no connection (AFAIK) between the two and you're welcome to use whichever one you favor. Competition fosters innovation and Teddit's release has motivated me to build Libreddit into an even more polished product.
|
||||
|
||||
If you are looking to compare, the biggest differences I have noticed are:
|
||||
- Libreddit is themed around Reddit's redesign whereas Teddit appears to stick much closer to Reddit's old design. This may suit some users better as design is always subjective.
|
||||
- Libreddit is written in Rust for speed and memory safety. It uses Actix Web, which was [benchmarked as the fastest web server for single queries](https://www.techempower.com/benchmarks/#hw=ph&test=db).
|
||||
- Unlike Teddit (at the time of writing this), Libreddit does not require a Reddit API key to host.
|
||||
- Currently, Teddit does not support hosting on Docker whereas Libreddit does.
|
||||
- Libreddit lacks certain functionality that Teddit offers, primarily: nested comments, configuration & multireddits. If these are features that you need, Teddit may suit your needs better.
|
||||
|
||||
## Instances
|
||||
|
||||
Feel free to [open an issue](https://github.com/spikecodes/libreddit/issues/new) to have your selfhosted instance listed here!
|
||||
|
||||
- [libredd.it](https://libredd.it) 🇺🇸 (Thank you to [YeapGuy](https://github.com/YeapGuy)!)
|
||||
- [libreddit.spike.codes](https://libreddit.spike.codes) 🇺🇸
|
||||
|
||||
## Deploy an Instance
|
||||
|
||||
Once installed, deploy Libreddit (unless you're using Docker) by running:
|
||||
|
||||
```
|
||||
libreddit
|
||||
```
|
||||
|
||||
Specify a custom address for the server by passing the `-a` or `--address` argument:
|
||||
```
|
||||
libreddit --address=0.0.0.0:8111
|
||||
```
|
||||
## Installation
|
||||
|
||||
### A) Cargo
|
||||
|
||||
@ -99,10 +76,32 @@ Install:
|
||||
yay -S libreddit-git
|
||||
```
|
||||
|
||||
## Building
|
||||
### D) GitHub Releases
|
||||
|
||||
If you're on Linux and none of these methods work for you, you can grab a Linux binary from [the newest release](https://github.com/spikecodes/libreddit/releases/latest).
|
||||
|
||||
## Deploy an Instance
|
||||
|
||||
Once installed, deploy Libreddit (unless you're using Docker) by running:
|
||||
|
||||
```
|
||||
libreddit
|
||||
```
|
||||
|
||||
Specify a custom address for the server by passing the `-a` or `--address` argument:
|
||||
```
|
||||
libreddit --address=0.0.0.0:8111
|
||||
```
|
||||
|
||||
To disable the media proxy built into Libreddit, run:
|
||||
```
|
||||
libreddit --no-default-features
|
||||
```
|
||||
|
||||
## Building from Source
|
||||
|
||||
```
|
||||
git clone https://github.com/spikecodes/libreddit
|
||||
cd libreddit
|
||||
cargo run
|
||||
```
|
||||
```
|
||||
|
@ -1,4 +1,4 @@
|
||||
edition = "2018"
|
||||
tab_spaces = 2
|
||||
hard_tabs = true
|
||||
max_width = 200
|
||||
max_width = 175
|
17
src/main.rs
17
src/main.rs
@ -4,21 +4,20 @@ use actix_web::{get, App, HttpResponse, HttpServer};
|
||||
// Reference local files
|
||||
mod popular;
|
||||
mod post;
|
||||
mod proxy;
|
||||
mod subreddit;
|
||||
mod user;
|
||||
mod proxy;
|
||||
mod utils;
|
||||
|
||||
// Create Services
|
||||
#[get("/style.css")]
|
||||
async fn style() -> HttpResponse {
|
||||
let file = std::fs::read_to_string("static/style.css").expect("ERROR: Could not read style.css");
|
||||
HttpResponse::Ok().content_type("text/css").body(file)
|
||||
HttpResponse::Ok().content_type("text/css").body(include_str!("../static/style.css"))
|
||||
}
|
||||
|
||||
#[get("/robots.txt")]
|
||||
async fn robots() -> HttpResponse {
|
||||
let file = std::fs::read_to_string("static/robots.txt").expect("ERROR: Could not read robots.txt");
|
||||
HttpResponse::Ok().body(file)
|
||||
HttpResponse::Ok().body(include_str!("../static/robots.txt"))
|
||||
}
|
||||
|
||||
#[get("/favicon.ico")]
|
||||
@ -32,9 +31,11 @@ async fn main() -> std::io::Result<()> {
|
||||
let mut address = "0.0.0.0:8080".to_string();
|
||||
|
||||
if args.len() > 1 {
|
||||
if args[1].starts_with("--address=") || args[1].starts_with("-a=") {
|
||||
let split: Vec<&str> = args[1].split("=").collect();
|
||||
address = split[1].to_string();
|
||||
for arg in args {
|
||||
if arg.starts_with("--address=") || arg.starts_with("-a=") {
|
||||
let split: Vec<&str> = arg.split("=").collect();
|
||||
address = split[1].to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,11 +1,8 @@
|
||||
// CRATES
|
||||
use actix_web::{get, web, HttpResponse, Result};
|
||||
use crate::utils::{fetch_posts, ErrorTemplate, Params, Post};
|
||||
use actix_web::{get, http::StatusCode, web, HttpResponse, Result};
|
||||
use askama::Template;
|
||||
|
||||
#[path = "utils.rs"]
|
||||
mod utils;
|
||||
use utils::{fetch_posts, ErrorTemplate, Params, Post};
|
||||
|
||||
// STRUCTS
|
||||
#[derive(Template)]
|
||||
#[template(path = "popular.html", escape = "none")]
|
||||
@ -37,7 +34,7 @@ async fn render(sub_name: String, sort: Option<String>, ends: (Option<String>, O
|
||||
}
|
||||
.render()
|
||||
.unwrap();
|
||||
Ok(HttpResponse::Ok().status(actix_web::http::StatusCode::NOT_FOUND).content_type("text/html").body(s))
|
||||
Ok(HttpResponse::Ok().status(StatusCode::NOT_FOUND).content_type("text/html").body(s))
|
||||
} else {
|
||||
let items = items_result.unwrap();
|
||||
|
||||
|
58
src/post.rs
58
src/post.rs
@ -1,13 +1,10 @@
|
||||
// CRATES
|
||||
use actix_web::{get, web, HttpResponse, Result};
|
||||
use crate::utils::{format_url, request, val, Comment, ErrorTemplate, Flair, Params, Post};
|
||||
use actix_web::{get, http::StatusCode, web, HttpResponse, Result};
|
||||
use askama::Template;
|
||||
use chrono::{TimeZone, Utc};
|
||||
use pulldown_cmark::{html, Options, Parser};
|
||||
|
||||
#[path = "utils.rs"]
|
||||
mod utils;
|
||||
use utils::{request, val, Comment, ErrorTemplate, Flair, Params, Post};
|
||||
|
||||
// STRUCTS
|
||||
#[derive(Template)]
|
||||
#[template(path = "post.html", escape = "none")]
|
||||
@ -19,7 +16,7 @@ struct PostTemplate {
|
||||
|
||||
async fn render(id: String, sort: String) -> Result<HttpResponse> {
|
||||
// Log the post ID being fetched
|
||||
println!("id: {}", id);
|
||||
dbg!(&id);
|
||||
|
||||
// Build the Reddit JSON API url
|
||||
let url: String = format!("https://reddit.com/{}.json?sort={}", id, sort);
|
||||
@ -34,15 +31,15 @@ async fn render(id: String, sort: String) -> Result<HttpResponse> {
|
||||
}
|
||||
.render()
|
||||
.unwrap();
|
||||
return Ok(HttpResponse::Ok().status(actix_web::http::StatusCode::NOT_FOUND).content_type("text/html").body(s));
|
||||
return Ok(HttpResponse::Ok().status(StatusCode::NOT_FOUND).content_type("text/html").body(s));
|
||||
}
|
||||
|
||||
// Otherwise, grab the JSON output from the request
|
||||
let res = req.unwrap();
|
||||
|
||||
// Parse the JSON into Post and Comment structs
|
||||
let post = parse_post(res.clone()).await;
|
||||
let comments = parse_comments(res).await;
|
||||
let post = parse_post(res[0].clone()).await;
|
||||
let comments = parse_comments(res[1].clone()).await;
|
||||
|
||||
// Use the Post and Comment structs to generate a website to show users
|
||||
let s = PostTemplate {
|
||||
@ -70,25 +67,23 @@ async fn page(web::Path((_sub, id)): web::Path<(String, String)>, params: web::Q
|
||||
}
|
||||
|
||||
// UTILITIES
|
||||
async fn media(data: &serde_json::Value) -> String {
|
||||
let post_hint: &str = data["data"]["post_hint"].as_str().unwrap_or("");
|
||||
let has_media: bool = data["data"]["media"].is_object();
|
||||
|
||||
let media: String = if !has_media {
|
||||
format!(r#"<h4 class="post_body"><a href="{u}">{u}</a></h4>"#, u = data["data"]["url"].as_str().unwrap())
|
||||
async fn media(data: &serde_json::Value) -> (String, String) {
|
||||
let post_type: &str;
|
||||
let url = if !data["preview"]["reddit_video_preview"]["fallback_url"].is_null() {
|
||||
post_type = "video";
|
||||
format_url(data["preview"]["reddit_video_preview"]["fallback_url"].as_str().unwrap()).await
|
||||
} else if !data["secure_media"]["reddit_video"]["fallback_url"].is_null() {
|
||||
post_type = "video";
|
||||
format_url(data["secure_media"]["reddit_video"]["fallback_url"].as_str().unwrap()).await
|
||||
} else if data["post_hint"].as_str().unwrap_or("") == "image" {
|
||||
post_type = "image";
|
||||
format_url(data["preview"]["images"][0]["source"]["url"].as_str().unwrap()).await
|
||||
} else {
|
||||
format!(r#"<img class="post_image" src="/imageproxy/{}.png"/>"#, data["data"]["url"].as_str().unwrap())
|
||||
post_type = "link";
|
||||
data["url"].as_str().unwrap().to_string()
|
||||
};
|
||||
|
||||
match post_hint {
|
||||
"hosted:video" => format!(
|
||||
r#"<video class="post_image" src="/imageproxy/{}" controls/>"#,
|
||||
data["data"]["media"]["reddit_video"]["fallback_url"].as_str().unwrap()
|
||||
),
|
||||
"image" => format!(r#"<img class="post_image" src="/imageproxy/{}"/>"#, data["data"]["url"].as_str().unwrap()),
|
||||
"self" => String::from(""),
|
||||
_ => media,
|
||||
}
|
||||
(post_type.to_string(), url)
|
||||
}
|
||||
|
||||
async fn markdown_to_html(md: &str) -> String {
|
||||
@ -107,11 +102,13 @@ async fn markdown_to_html(md: &str) -> String {
|
||||
|
||||
// POSTS
|
||||
async fn parse_post(json: serde_json::Value) -> Result<Post, &'static str> {
|
||||
let post_data: &serde_json::Value = &json[0]["data"]["children"][0];
|
||||
let post_data: &serde_json::Value = &json["data"]["children"][0];
|
||||
|
||||
let unix_time: i64 = post_data["data"]["created_utc"].as_f64().unwrap().round() as i64;
|
||||
let score = post_data["data"]["score"].as_i64().unwrap();
|
||||
|
||||
let media = media(&post_data["data"]).await;
|
||||
|
||||
let post = Post {
|
||||
title: val(post_data, "title").await,
|
||||
community: val(post_data, "subreddit").await,
|
||||
@ -119,7 +116,8 @@ async fn parse_post(json: serde_json::Value) -> Result<Post, &'static str> {
|
||||
author: val(post_data, "author").await,
|
||||
url: val(post_data, "permalink").await,
|
||||
score: if score > 1000 { format!("{}k", score / 1000) } else { score.to_string() },
|
||||
media: media(post_data).await,
|
||||
post_type: media.0,
|
||||
media: media.1,
|
||||
time: Utc.timestamp(unix_time, 0).format("%b %e %Y %H:%M UTC").to_string(),
|
||||
flair: Flair(
|
||||
val(post_data, "link_flair_text").await,
|
||||
@ -137,7 +135,7 @@ async fn parse_post(json: serde_json::Value) -> Result<Post, &'static str> {
|
||||
|
||||
// COMMENTS
|
||||
async fn parse_comments(json: serde_json::Value) -> Result<Vec<Comment>, &'static str> {
|
||||
let comment_data = json[1]["data"]["children"].as_array().unwrap();
|
||||
let comment_data = json["data"]["children"].as_array().unwrap();
|
||||
|
||||
let mut comments: Vec<Comment> = Vec::new();
|
||||
|
||||
@ -146,7 +144,9 @@ async fn parse_comments(json: serde_json::Value) -> Result<Vec<Comment>, &'stati
|
||||
let score = comment["data"]["score"].as_i64().unwrap_or(0);
|
||||
let body = markdown_to_html(comment["data"]["body"].as_str().unwrap_or("")).await;
|
||||
|
||||
// println!("{}", body);
|
||||
// if comment["data"]["replies"].is_object() {
|
||||
// let replies = parse_comments(comment["data"]["replies"].clone()).await.unwrap();
|
||||
// }
|
||||
|
||||
comments.push(Comment {
|
||||
body: body,
|
||||
|
38
src/proxy.rs
38
src/proxy.rs
@ -1,14 +1,30 @@
|
||||
use actix_web::{get, web, HttpResponse, Result, client::Client, Error};
|
||||
use actix_web::{client::Client, get, web, Error, HttpResponse, Result};
|
||||
|
||||
#[cfg(feature = "proxy")]
|
||||
use base64::decode;
|
||||
|
||||
#[get("/imageproxy/{url:.*}")]
|
||||
async fn handler(web::Path(url): web::Path<String>) -> Result<HttpResponse> {
|
||||
dbg!(&url);
|
||||
let client = Client::default();
|
||||
client.get(url)
|
||||
.send()
|
||||
.await
|
||||
.map_err(Error::from)
|
||||
.and_then(|res| {
|
||||
Ok(HttpResponse::build(res.status()).streaming(res))
|
||||
})
|
||||
}
|
||||
if cfg!(feature = "proxy") {
|
||||
let media: String;
|
||||
|
||||
#[cfg(not(feature = "proxy"))]
|
||||
let media = url;
|
||||
|
||||
#[cfg(feature = "proxy")]
|
||||
match decode(url) {
|
||||
Ok(bytes) => media = String::from_utf8(bytes).unwrap(),
|
||||
Err(_e) => return Ok(HttpResponse::Ok().body("")),
|
||||
};
|
||||
|
||||
let client = Client::default();
|
||||
client
|
||||
.get(media.replace("&", "&"))
|
||||
.send()
|
||||
.await
|
||||
.map_err(Error::from)
|
||||
.and_then(|res| Ok(HttpResponse::build(res.status()).streaming(res)))
|
||||
} else {
|
||||
Ok(HttpResponse::Ok().body(""))
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,8 @@
|
||||
// CRATES
|
||||
use actix_web::{get, web, HttpResponse, Result};
|
||||
use crate::utils::{fetch_posts, format_url, request, val, ErrorTemplate, Params, Post, Subreddit};
|
||||
use actix_web::{get, http::StatusCode, web, HttpResponse, Result};
|
||||
use askama::Template;
|
||||
|
||||
#[path = "utils.rs"]
|
||||
mod utils;
|
||||
pub use utils::{request, val, fetch_posts, ErrorTemplate, Flair, Params, Post, Subreddit};
|
||||
|
||||
// STRUCTS
|
||||
#[derive(Template)]
|
||||
#[template(path = "subreddit.html", escape = "none")]
|
||||
@ -13,7 +10,7 @@ struct SubredditTemplate {
|
||||
sub: Subreddit,
|
||||
posts: Vec<Post>,
|
||||
sort: String,
|
||||
ends: (String, String)
|
||||
ends: (String, String),
|
||||
}
|
||||
|
||||
// SERVICES
|
||||
@ -45,7 +42,7 @@ pub async fn render(sub_name: String, sort: Option<String>, ends: (Option<String
|
||||
}
|
||||
.render()
|
||||
.unwrap();
|
||||
Ok(HttpResponse::Ok().status(actix_web::http::StatusCode::NOT_FOUND).content_type("text/html").body(s))
|
||||
Ok(HttpResponse::Ok().status(StatusCode::NOT_FOUND).content_type("text/html").body(s))
|
||||
} else {
|
||||
let mut sub = sub_result.unwrap();
|
||||
let items = items_result.unwrap();
|
||||
@ -60,7 +57,7 @@ pub async fn render(sub_name: String, sort: Option<String>, ends: (Option<String
|
||||
sub: sub,
|
||||
posts: items.0,
|
||||
sort: sorting,
|
||||
ends: (before, items.1)
|
||||
ends: (before, items.1),
|
||||
}
|
||||
.render()
|
||||
.unwrap();
|
||||
@ -85,16 +82,16 @@ async fn subreddit(sub: &String) -> Result<Subreddit, &'static str> {
|
||||
let res = req.unwrap();
|
||||
|
||||
let members = res["data"]["subscribers"].as_u64().unwrap_or(0);
|
||||
let active = res["data"]["accounts_active"].as_u64().unwrap_or(0);
|
||||
let active = res["data"]["accounts_active"].as_u64().unwrap_or(0);
|
||||
|
||||
let sub = Subreddit {
|
||||
name: val(&res, "display_name").await,
|
||||
title: val(&res, "title").await,
|
||||
description: val(&res, "public_description").await,
|
||||
icon: val(&res, "icon_img").await,
|
||||
icon: format_url(val(&res, "icon_img").await.as_str()).await,
|
||||
members: if members > 1000 { format!("{}k", members / 1000) } else { members.to_string() },
|
||||
active: if active > 1000 { format!("{}k", active / 1000) } else { active.to_string() },
|
||||
};
|
||||
|
||||
Ok(sub)
|
||||
}
|
||||
}
|
||||
|
11
src/user.rs
11
src/user.rs
@ -1,11 +1,8 @@
|
||||
// CRATES
|
||||
use actix_web::{get, web, HttpResponse, Result};
|
||||
use crate::utils::{fetch_posts, nested_val, request, ErrorTemplate, Params, Post, User};
|
||||
use actix_web::{get, http::StatusCode, web, HttpResponse, Result};
|
||||
use askama::Template;
|
||||
|
||||
#[path = "utils.rs"]
|
||||
mod utils;
|
||||
use utils::{nested_val, request, fetch_posts, ErrorTemplate, Params, Post, User};
|
||||
|
||||
// STRUCTS
|
||||
#[derive(Template)]
|
||||
#[template(path = "user.html", escape = "none")]
|
||||
@ -28,7 +25,7 @@ async fn render(username: String, sort: String) -> Result<HttpResponse> {
|
||||
}
|
||||
.render()
|
||||
.unwrap();
|
||||
Ok(HttpResponse::Ok().status(actix_web::http::StatusCode::NOT_FOUND).content_type("text/html").body(s))
|
||||
Ok(HttpResponse::Ok().status(StatusCode::NOT_FOUND).content_type("text/html").body(s))
|
||||
} else {
|
||||
let s = UserTemplate {
|
||||
user: user.unwrap(),
|
||||
@ -74,4 +71,4 @@ async fn user(name: &String) -> Result<User, &'static str> {
|
||||
banner: nested_val(&res, "subreddit", "banner_img").await,
|
||||
description: nested_val(&res, "subreddit", "public_description").await,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
59
src/utils.rs
59
src/utils.rs
@ -2,6 +2,11 @@
|
||||
// CRATES
|
||||
//
|
||||
use chrono::{TimeZone, Utc};
|
||||
use serde_json::{from_str, Value};
|
||||
// use surf::{client, get, middleware::Redirect};
|
||||
|
||||
#[cfg(feature = "proxy")]
|
||||
use base64::encode;
|
||||
|
||||
//
|
||||
// STRUCTS
|
||||
@ -19,9 +24,10 @@ pub struct Post {
|
||||
pub author: String,
|
||||
pub url: String,
|
||||
pub score: String,
|
||||
pub post_type: String,
|
||||
pub media: String,
|
||||
pub time: String,
|
||||
pub flair: Flair
|
||||
pub flair: Flair,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
@ -30,7 +36,7 @@ pub struct Comment {
|
||||
pub body: String,
|
||||
pub author: String,
|
||||
pub score: String,
|
||||
pub time: String
|
||||
pub time: String,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
@ -40,7 +46,7 @@ pub struct User {
|
||||
pub icon: String,
|
||||
pub karma: i64,
|
||||
pub banner: String,
|
||||
pub description: String
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
@ -51,7 +57,7 @@ pub struct Subreddit {
|
||||
pub description: String,
|
||||
pub icon: String,
|
||||
pub members: String,
|
||||
pub active: String
|
||||
pub active: String,
|
||||
}
|
||||
|
||||
// Parser for query params, used in sorting (eg. /r/rust/?sort=hot)
|
||||
@ -59,14 +65,26 @@ pub struct Subreddit {
|
||||
pub struct Params {
|
||||
pub sort: Option<String>,
|
||||
pub after: Option<String>,
|
||||
pub before: Option<String>
|
||||
pub before: Option<String>,
|
||||
}
|
||||
|
||||
// Error template
|
||||
#[derive(askama::Template)]
|
||||
#[template(path = "error.html", escape = "none")]
|
||||
pub struct ErrorTemplate {
|
||||
pub message: String
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
//
|
||||
// URL HANDLING
|
||||
//
|
||||
|
||||
pub async fn format_url(url: &str) -> String {
|
||||
#[cfg(feature = "proxy")]
|
||||
return "/imageproxy/".to_string() + encode(url).as_str();
|
||||
|
||||
#[cfg(not(feature = "proxy"))]
|
||||
return url.to_string();
|
||||
}
|
||||
|
||||
//
|
||||
@ -105,7 +123,7 @@ pub async fn fetch_posts(url: String, fallback_title: String) -> Result<(Vec<Pos
|
||||
|
||||
for post in post_list.iter() {
|
||||
let img = if val(post, "thumbnail").await.starts_with("https:/") {
|
||||
val(post, "thumbnail").await
|
||||
format_url(val(post, "thumbnail").await.as_str()).await
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
@ -119,6 +137,7 @@ pub async fn fetch_posts(url: String, fallback_title: String) -> Result<(Vec<Pos
|
||||
body: val(post, "body").await,
|
||||
author: val(post, "author").await,
|
||||
score: if score > 1000 { format!("{}k", score / 1000) } else { score.to_string() },
|
||||
post_type: "link".to_string(),
|
||||
media: img,
|
||||
url: val(post, "permalink").await,
|
||||
time: Utc.timestamp(unix_time, 0).format("%b %e '%y").to_string(),
|
||||
@ -157,28 +176,26 @@ pub async fn request(url: String) -> Result<serde_json::Value, &'static str> {
|
||||
// let body = std::str::from_utf8(res.as_ref())?; // .as_ref converts Bytes to [u8]
|
||||
|
||||
// --- surf ---
|
||||
let req = surf::get(&url).header("User-Agent", "libreddit");
|
||||
let client = surf::client().with(surf::middleware::Redirect::new(5));
|
||||
let mut res = client.send(req).await.unwrap();
|
||||
let success = res.status().is_success();
|
||||
let body = res.body_string().await.unwrap();
|
||||
|
||||
dbg!(url.clone());
|
||||
// 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
|
||||
// let success = res.status().is_success();
|
||||
// // Read the body of the response
|
||||
// let body = res.text().await.unwrap();
|
||||
let res = reqwest::get(&url).await.unwrap();
|
||||
// Read the status from the response
|
||||
let success = res.status().is_success();
|
||||
// Read the body of the response
|
||||
let body = res.text().await.unwrap();
|
||||
|
||||
// Parse the response from Reddit as JSON
|
||||
let json: serde_json::Value = serde_json::from_str(body.as_str()).unwrap_or(serde_json::Value::Null);
|
||||
let json: Value = from_str(body.as_str()).unwrap_or(Value::Null);
|
||||
|
||||
if !success {
|
||||
println!("! {} - {}", url, "Page not found");
|
||||
Err("Page not found")
|
||||
} else if json == serde_json::Value::Null {
|
||||
} else if json == Value::Null {
|
||||
println!("! {} - {}", url, "Failed to parse page JSON data");
|
||||
Err("Failed to parse page JSON data")
|
||||
} else {
|
||||
|
@ -180,18 +180,10 @@ span {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.post_right > p > span, .comment_right > p > span {
|
||||
.datetime {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.post_title {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.post_upvote {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.post_subreddit {
|
||||
font-weight: bold;
|
||||
}
|
||||
@ -211,16 +203,21 @@ span {
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.post_right > p {
|
||||
opacity: 0.75;
|
||||
.post_right > a {
|
||||
color: aqua;
|
||||
}
|
||||
|
||||
.post_image {
|
||||
max-width: 500px;
|
||||
.post_right > p {
|
||||
opacity: 0.75;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.post_media {
|
||||
max-width: 90%;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.post_image[src=""] {
|
||||
.post_media[src=""] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@ -234,7 +231,7 @@ span {
|
||||
margin-top: 1.5em;
|
||||
}
|
||||
|
||||
.post_body a {
|
||||
.post_body > a {
|
||||
text-decoration: underline;
|
||||
color: aqua;
|
||||
}
|
||||
@ -245,19 +242,21 @@ span {
|
||||
flex-shrink: 0;
|
||||
padding: 10px;
|
||||
border-radius: 15px;
|
||||
max-width: 20%;
|
||||
}
|
||||
|
||||
.post_thumbnail[src=""] {
|
||||
border: none;
|
||||
display: none;
|
||||
}
|
||||
|
||||
small {
|
||||
background: aqua;
|
||||
color: black;
|
||||
padding: 5px;
|
||||
border-radius: 5px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
background: aqua;
|
||||
color: black;
|
||||
padding: 5px;
|
||||
margin-right: 5px;
|
||||
border-radius: 5px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Comment */
|
||||
@ -366,4 +365,32 @@ table {
|
||||
td, th {
|
||||
border: 1px #333 solid;
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
/* Mobile */
|
||||
|
||||
@media screen and (max-width: 480px) {
|
||||
.post {
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
.post_left {
|
||||
border-radius: 0px 0px 5px 5px;
|
||||
}
|
||||
|
||||
.post_right {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.post_score {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.post_thumbnail {
|
||||
max-width: initial;
|
||||
}
|
||||
|
||||
.datetime {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
@ -4,6 +4,7 @@
|
||||
{% block head %}
|
||||
<title>{% block title %}{% endblock %}</title>
|
||||
<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 http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<link rel="stylesheet" href="/style.css">
|
||||
{% block sortstyle %}
|
||||
|
@ -5,22 +5,21 @@
|
||||
<div id="sort_hot"><a href="?sort=hot">Hot</a></div>
|
||||
<div id="sort_top"><a href="?sort=top">Top</a></div>
|
||||
<div id="sort_new"><a href="?sort=new">New</a></div>
|
||||
<div id="sort_rising"><a href="?sort=rising">Rising</a></div>
|
||||
</div>
|
||||
{% for post in posts %}
|
||||
<div class="post">
|
||||
<div class="post_left">
|
||||
<button class="post_upvote">↑</button>
|
||||
<h3 class="post_score">{{ post.score }}</h3>
|
||||
<button class="post_upvote">↓</button>
|
||||
</div>
|
||||
<div class="post_right">
|
||||
<p>
|
||||
<h4>
|
||||
<b><a class="post_subreddit" href="/r/{{ post.community }}">r/{{ post.community }}</a></b>
|
||||
•
|
||||
Posted by
|
||||
<a class="post_author" href="/u/{{ post.author }}">u/{{ post.author }}</a>
|
||||
<span style="float: right;">{{ post.time }}</span>
|
||||
</p>
|
||||
<span class="datetime" style="float: right;">{{ post.time }}</span>
|
||||
</h4>
|
||||
<h3 class="post_title">
|
||||
{% if post.flair.0 != "" %}
|
||||
<small style="color:{{ post.flair.2 }}; background:{{ post.flair.1 }}">{{ post.flair.0 }}</small>
|
||||
@ -41,4 +40,4 @@
|
||||
<a href="?after={{ ends.1 }}">NEXT</a>
|
||||
{% endif %}
|
||||
</footer>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
@ -7,25 +7,29 @@
|
||||
{% block content %}
|
||||
<div class="post highlighted">
|
||||
<div class="post_left">
|
||||
<button class="post_upvote">↑</button>
|
||||
<h3 class="post_score">{{ post.score }}</h3>
|
||||
<button class="post_upvote">↓</button>
|
||||
</div>
|
||||
<div class="post_right">
|
||||
<p>
|
||||
<h4>
|
||||
<b><a class="post_subreddit" href="/r/{{ post.community }}">r/{{ post.community }}</a></b>
|
||||
•
|
||||
Posted by
|
||||
<a class="post_author" href="/u/{{ post.author }}">u/{{ post.author }}</a>
|
||||
<span>{{ post.time }}</span>
|
||||
</p>
|
||||
<span class="datetime">{{ post.time }}</span>
|
||||
</h4>
|
||||
<h3 class="post_title">
|
||||
{{ post.title }}
|
||||
{% if post.flair.0 != "" %}
|
||||
<small style="color:{{ post.flair.2 }}; background:{{ post.flair.1 }}">{{ post.flair.0 }}</small>
|
||||
{% endif %}
|
||||
</h3>
|
||||
{{ post.media }}
|
||||
{% if post.post_type == "image" %}
|
||||
<img class="post_media" src="{{ post.media }}"/>
|
||||
{% else if post.post_type == "video" %}
|
||||
<video class="post_media" src="{{ post.media }}" controls autoplay loop>
|
||||
{% else if post.post_type == "link" %}
|
||||
<a href="{{ post.media }}">{{ post.media }}</a>
|
||||
{% endif %}
|
||||
<h4 class="post_body">{{ post.body }}</h4>
|
||||
</div>
|
||||
</div>
|
||||
@ -39,14 +43,14 @@
|
||||
{% for comment in comments %}
|
||||
<div class="comment">
|
||||
<div class="comment_left">
|
||||
<button class="comment_upvote">↑</button>
|
||||
<div class="comment_upvote">↑</div>
|
||||
<h3 class="comment_score">{{ comment.score }}</h3>
|
||||
</div>
|
||||
<div class="comment_right">
|
||||
<p>
|
||||
<h4>
|
||||
Posted by <a class="comment_author" href="/u/{{ comment.author }}">u/{{ comment.author }}</a>
|
||||
<span>{{ comment.time }}</span>
|
||||
</p>
|
||||
<span class="datetime">{{ comment.time }}</span>
|
||||
</h4>
|
||||
<h4 class="comment_body">{{ comment.body }}</h4>
|
||||
</div>
|
||||
</div><br>
|
||||
|
@ -28,18 +28,16 @@
|
||||
{% for post in posts %}
|
||||
<div class="post">
|
||||
<div class="post_left">
|
||||
<button class="post_upvote">↑</button>
|
||||
<h3 class="post_score">{{ post.score }}</h3>
|
||||
<button class="post_upvote">↓</button>
|
||||
</div>
|
||||
<div class="post_right">
|
||||
<p>
|
||||
<h4>
|
||||
<b><a class="post_subreddit" href="/r/{{ post.community }}">r/{{ sub.name }}</a></b>
|
||||
•
|
||||
Posted by
|
||||
<a class="post_author" href="/u/{{ post.author }}">u/{{ post.author }}</a>
|
||||
<span>{{ post.time }}</span>
|
||||
</p>
|
||||
<span class="datetime">{{ post.time }}</span>
|
||||
</h4>
|
||||
<h3 class="post_title">
|
||||
{% if post.flair.0 != "" %}
|
||||
<small style="color:{{ post.flair.2 }}; background:{{ post.flair.1 }}">{{ post.flair.0 }}</small>
|
||||
|
@ -28,18 +28,16 @@
|
||||
{% if post.title != "Comment" %}
|
||||
<div class='post'>
|
||||
<div class="post_left">
|
||||
<button class="post_upvote">↑</button>
|
||||
<h3 class="post_score">{{ post.score }}</h3>
|
||||
<button class="post_upvote">↓</button>
|
||||
</div>
|
||||
<div class="post_right">
|
||||
<p>
|
||||
<h4>
|
||||
<b><a class="post_subreddit" href="/r/{{ post.community }}">r/{{ post.community }}</a></b>
|
||||
•
|
||||
Posted by
|
||||
<a class="post_author" href="/u/{{ post.author }}">u/{{ post.author }}</a>
|
||||
<span style="float: right;">{{ post.time }}</span>
|
||||
</p>
|
||||
<span class="datetime" style="float: right;">{{ post.time }}</span>
|
||||
</h4>
|
||||
<h3 class="post_title">
|
||||
{% if post.flair.0 == "Comment" %}
|
||||
{% else if post.flair.0 == "" %}
|
||||
@ -54,14 +52,14 @@
|
||||
{% else %}
|
||||
<div class="comment">
|
||||
<div class="comment_left">
|
||||
<button class="comment_upvote">↑</button>
|
||||
<div class="comment_upvote">↑</div>
|
||||
<h3 class="comment_score">{{ post.score }}</h3>
|
||||
</div>
|
||||
<div class="comment_right">
|
||||
<p>
|
||||
<h4>
|
||||
COMMENT
|
||||
<span>{{ post.time }}</span>
|
||||
</p>
|
||||
<span class="datetime">{{ post.time }}</span>
|
||||
</h4>
|
||||
<h4 class="comment_body">{{ post.body }}</h4>
|
||||
</div>
|
||||
</div><br>
|
||||
|
Reference in New Issue
Block a user