Actix Rewrite
This commit is contained in:
commit
9bd1b247bd
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/target
|
2581
Cargo.lock
generated
Normal file
2581
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
17
Cargo.toml
Normal file
17
Cargo.toml
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
[package]
|
||||||
|
name = "libreddit-actix"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["spikecodes <19519553+spikecodes@users.noreply.github.com>"]
|
||||||
|
edition = "2018"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
actix-web = "3"
|
||||||
|
actix-files = "0.4.0"
|
||||||
|
askama = "0.9"
|
||||||
|
serde_json = "1.0"
|
||||||
|
reqwest = { version = "0.10", features = ["blocking"] }
|
||||||
|
comrak = "0.8"
|
||||||
|
chrono = "0.4"
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
askama = "0.9"
|
1
rustfmt.toml
Normal file
1
rustfmt.toml
Normal file
@ -0,0 +1 @@
|
|||||||
|
tab_spaces = 2
|
49
src/main.rs
Normal file
49
src/main.rs
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
// Import Crates
|
||||||
|
extern crate comrak;
|
||||||
|
use actix_files::NamedFile;
|
||||||
|
use actix_web::{get, App, HttpServer, HttpResponse, Result};
|
||||||
|
|
||||||
|
// Reference local files
|
||||||
|
mod user;
|
||||||
|
mod popular;
|
||||||
|
mod post;
|
||||||
|
mod subreddit;
|
||||||
|
|
||||||
|
// Create Services
|
||||||
|
#[get("/style.css")]
|
||||||
|
async fn style() -> Result<NamedFile> {
|
||||||
|
let file = NamedFile::open("static/style.css");
|
||||||
|
Ok(file?)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/favicon.ico")]
|
||||||
|
async fn favicon() -> HttpResponse {
|
||||||
|
HttpResponse::Ok().body("")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[actix_web::main]
|
||||||
|
async fn main() -> std::io::Result<()> {
|
||||||
|
// start http server
|
||||||
|
HttpServer::new(|| {
|
||||||
|
App::new()
|
||||||
|
// GENERAL SERVICES
|
||||||
|
.service(style)
|
||||||
|
.service(favicon)
|
||||||
|
// POST SERVICES
|
||||||
|
.service(post::short)
|
||||||
|
.service(post::page)
|
||||||
|
.service(post::sorted)
|
||||||
|
// SUBREDDIT SERVICES
|
||||||
|
.service(subreddit::page)
|
||||||
|
.service(subreddit::sorted)
|
||||||
|
// POPULAR SERVICES
|
||||||
|
.service(popular::page)
|
||||||
|
// .service(popular::sorted)
|
||||||
|
// USER SERVICES
|
||||||
|
.service(user::page)
|
||||||
|
})
|
||||||
|
.bind("127.0.0.1:8080")?
|
||||||
|
.run()
|
||||||
|
.await
|
||||||
|
}
|
44
src/popular.rs
Normal file
44
src/popular.rs
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
// CRATES
|
||||||
|
extern crate comrak;
|
||||||
|
use actix_web::{get, HttpResponse, Result};
|
||||||
|
use askama::Template;
|
||||||
|
|
||||||
|
#[path = "subreddit.rs"] mod subreddit;
|
||||||
|
|
||||||
|
// STRUCTS
|
||||||
|
#[derive(Template)]
|
||||||
|
#[template(path = "popular.html", escape = "none")]
|
||||||
|
struct PopularTemplate {
|
||||||
|
posts: Vec<subreddit::Post>,
|
||||||
|
sort: String
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/")]
|
||||||
|
pub async fn page() -> Result<HttpResponse> {
|
||||||
|
render("popular".to_string(), "hot".to_string()).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn render(sub_name: String, sort: String) -> Result<HttpResponse> {
|
||||||
|
let posts: Vec<subreddit::Post> = subreddit::posts(sub_name, &sort).await;
|
||||||
|
|
||||||
|
let s = PopularTemplate {
|
||||||
|
posts: posts,
|
||||||
|
sort: sort
|
||||||
|
}
|
||||||
|
.render()
|
||||||
|
.unwrap();
|
||||||
|
Ok(HttpResponse::Ok().content_type("text/html").body(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
// #[get("/?<sort>")]
|
||||||
|
// pub fn sorted(sort: String) -> Template {
|
||||||
|
// println!("{}", sort);
|
||||||
|
// let posts: Vec<subreddit::Post> = subreddit::posts(&"popular".to_string(), &sort).unwrap();
|
||||||
|
|
||||||
|
// let mut context = std::collections::HashMap::new();
|
||||||
|
// context.insert("about", String::new());
|
||||||
|
// context.insert("sort", sort);
|
||||||
|
// context.insert("posts", subreddit::posts_html(posts));
|
||||||
|
|
||||||
|
// Template::render("popular", context)
|
||||||
|
// }
|
179
src/post.rs
Normal file
179
src/post.rs
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
// CRATES
|
||||||
|
extern crate comrak;
|
||||||
|
use actix_web::{get, web, HttpResponse, Result};
|
||||||
|
use askama::Template;
|
||||||
|
use comrak::{markdown_to_html, ComrakOptions};
|
||||||
|
use chrono::{TimeZone, Utc};
|
||||||
|
|
||||||
|
// STRUCTS
|
||||||
|
#[derive(Template)]
|
||||||
|
#[template(path = "post.html", escape = "none")]
|
||||||
|
struct PostTemplate {
|
||||||
|
comments: Vec<Comment>,
|
||||||
|
post: Post,
|
||||||
|
sort: String
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Post {
|
||||||
|
pub title: String,
|
||||||
|
pub community: String,
|
||||||
|
pub body: String,
|
||||||
|
pub author: String,
|
||||||
|
pub url: String,
|
||||||
|
pub score: String,
|
||||||
|
pub media: String,
|
||||||
|
pub time: String
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Comment {
|
||||||
|
pub body: String,
|
||||||
|
pub author: String,
|
||||||
|
pub score: i64,
|
||||||
|
pub time: String
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn render(id: String, sort: String) -> Result<HttpResponse> {
|
||||||
|
println!("id: {}", id);
|
||||||
|
let post: Post = fetch_post(&id).await;
|
||||||
|
let comments: Vec<Comment> = fetch_comments(id, &sort).await.unwrap();
|
||||||
|
|
||||||
|
let s = PostTemplate {
|
||||||
|
comments: comments,
|
||||||
|
post: post,
|
||||||
|
sort: sort
|
||||||
|
}
|
||||||
|
.render()
|
||||||
|
.unwrap();
|
||||||
|
Ok(HttpResponse::Ok().content_type("text/html").body(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SERVICES
|
||||||
|
#[get("/{id}")]
|
||||||
|
async fn short(web::Path(id): web::Path<String>) -> Result<HttpResponse> {
|
||||||
|
render(id.to_string(), "confidence".to_string()).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/r/{sub}/comments/{id}/{title}")]
|
||||||
|
async fn page(web::Path((_sub, id)): web::Path<(String, String)>) -> Result<HttpResponse> {
|
||||||
|
render(id.to_string(), "confidence".to_string()).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/r/{sub}/comments/{id}/{title}/{sort}")]
|
||||||
|
async fn sorted(web::Path((_sub, id, _title, sort)): web::Path<(String, String, String, String)>) -> Result<HttpResponse> {
|
||||||
|
render(id.to_string(), sort).await
|
||||||
|
}
|
||||||
|
|
||||||
|
// UTILITIES
|
||||||
|
async fn val (j: &serde_json::Value, k: &str) -> String { String::from(j["data"][k].as_str().unwrap_or("")) }
|
||||||
|
|
||||||
|
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()) }
|
||||||
|
else { format!(r#"<img class="post_image" src="{}.png"/>"#, data["data"]["url"].as_str().unwrap()) };
|
||||||
|
|
||||||
|
match post_hint {
|
||||||
|
"hosted:video" => format!(r#"<video class="post_image" src="{}" controls/>"#, data["data"]["media"]["reddit_video"]["fallback_url"].as_str().unwrap()),
|
||||||
|
"image" => format!(r#"<img class="post_image" src="{}"/>"#, data["data"]["url"].as_str().unwrap()),
|
||||||
|
"self" => String::from(""),
|
||||||
|
_ => media
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// POSTS
|
||||||
|
// async fn post_html (post: Post) -> String {
|
||||||
|
// format!(r#"
|
||||||
|
// <div class="post" style="border: 2px solid #555;background: #222;">
|
||||||
|
// <div class="post_left" style="background: #333;">
|
||||||
|
// <button class="post_upvote">↑</button>
|
||||||
|
// <h3 class="post_score">{}</h3>
|
||||||
|
// <button class="post_upvote">↓</button>
|
||||||
|
// </div>
|
||||||
|
// <div class="post_right">
|
||||||
|
// <p>
|
||||||
|
// <b><a class="post_subreddit" href="/r/{sub}">r/{sub}</a></b>
|
||||||
|
// •
|
||||||
|
// Posted by
|
||||||
|
// <a class="post_author" href="/u/{author}">u/{author}</a>
|
||||||
|
// <span style="float: right;">{time}</span>
|
||||||
|
// </p>
|
||||||
|
// <h3 class="post_title">{t}</h3>
|
||||||
|
// {media}
|
||||||
|
// <h4 class="post_body">{b}</h4>
|
||||||
|
// </div>
|
||||||
|
// </div><br>
|
||||||
|
// "#, if post.score>1000{format!("{}k", post.score/1000)} else {post.score.to_string()}, sub = post.community,
|
||||||
|
// author = post.author, t = post.title, media = post.media, b = post.body, time = post.time)
|
||||||
|
// }
|
||||||
|
|
||||||
|
async fn fetch_post (id: &String) -> Post {
|
||||||
|
let url: String = format!("https://reddit.com/{}.json", id);
|
||||||
|
let resp: String = reqwest::get(&url).await.unwrap().text().await.unwrap();
|
||||||
|
|
||||||
|
let data: serde_json::Value = serde_json::from_str(resp.as_str()).expect("Failed to parse JSON");
|
||||||
|
|
||||||
|
let post_data: &serde_json::Value = &data[0]["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();
|
||||||
|
|
||||||
|
Post {
|
||||||
|
title: val(post_data, "title").await,
|
||||||
|
community: val(post_data, "subreddit").await,
|
||||||
|
body: markdown_to_html(post_data["data"]["selftext"].as_str().unwrap(), &ComrakOptions::default()),
|
||||||
|
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,
|
||||||
|
time: Utc.timestamp(unix_time, 0).format("%b %e %Y %H:%M UTC").to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// COMMENTS
|
||||||
|
async fn fetch_comments (id: String, sort: &String) -> Result<Vec<Comment>, Box<dyn std::error::Error>> {
|
||||||
|
let url: String = format!("https://reddit.com/{}.json?sort={}", id, sort);
|
||||||
|
let resp: String = reqwest::get(&url).await?.text().await?;
|
||||||
|
|
||||||
|
let data: serde_json::Value = serde_json::from_str(resp.as_str())?;
|
||||||
|
|
||||||
|
let comment_data = data[1]["data"]["children"].as_array().unwrap();
|
||||||
|
|
||||||
|
let mut comments: Vec<Comment> = Vec::new();
|
||||||
|
|
||||||
|
for comment in comment_data.iter() {
|
||||||
|
let unix_time: i64 = comment["data"]["created_utc"].as_f64().unwrap_or(0.0).round() as i64;
|
||||||
|
comments.push(Comment {
|
||||||
|
body: markdown_to_html(comment["data"]["body"].as_str().unwrap_or(""), &ComrakOptions::default()),
|
||||||
|
author: val(comment, "author").await,
|
||||||
|
score: comment["data"]["score"].as_i64().unwrap_or(0),
|
||||||
|
time: Utc.timestamp(unix_time, 0).format("%b %e %Y %H:%M UTC").to_string()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(comments)
|
||||||
|
}
|
||||||
|
|
||||||
|
// async fn comments_html (comments: Vec<Comment>) -> String {
|
||||||
|
// let mut html: Vec<String> = Vec::new();
|
||||||
|
// for comment in comments.iter() {
|
||||||
|
// let hc: String = format!(r#"
|
||||||
|
// <div class="post">
|
||||||
|
// <div class="post_left">
|
||||||
|
// <button class="post_upvote">↑</button>
|
||||||
|
// <h3 class="post_score">{}</h3>
|
||||||
|
// <button class="post_upvote">↓</button>
|
||||||
|
// </div>
|
||||||
|
// <div class="post_right">
|
||||||
|
// <p>
|
||||||
|
// Posted by <a class="post_author" href="/u/{author}">u/{author}</a>
|
||||||
|
// <span style="float: right;">{time}</span>
|
||||||
|
// </p>
|
||||||
|
// <h4 class="post_body">{t}</h4>
|
||||||
|
// </div>
|
||||||
|
// </div><br>
|
||||||
|
// "#, if comment.score>1000{format!("{}k", comment.score/1000)} else {comment.score.to_string()},
|
||||||
|
// author = comment.author, t = comment.body, time = comment.time);
|
||||||
|
// html.push(hc)
|
||||||
|
// }; html.join("\n")
|
||||||
|
// }
|
110
src/subreddit.rs
Normal file
110
src/subreddit.rs
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
// CRATES
|
||||||
|
extern crate comrak;
|
||||||
|
use actix_web::{get, web, HttpResponse, Result};
|
||||||
|
use askama::Template;
|
||||||
|
use chrono::{TimeZone, Utc};
|
||||||
|
|
||||||
|
// STRUCTS
|
||||||
|
#[derive(Template)]
|
||||||
|
#[template(path = "subreddit.html", escape = "none")]
|
||||||
|
struct SubredditTemplate {
|
||||||
|
sub: Subreddit,
|
||||||
|
posts: Vec<Post>,
|
||||||
|
sort: String
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Post {
|
||||||
|
pub title: String,
|
||||||
|
pub community: String,
|
||||||
|
pub author: String,
|
||||||
|
pub score: String,
|
||||||
|
pub image: String,
|
||||||
|
pub url: String,
|
||||||
|
pub time: String
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Subreddit {
|
||||||
|
pub name: String,
|
||||||
|
pub title: String,
|
||||||
|
pub description: String,
|
||||||
|
pub icon: String
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn render(sub_name: String, sort: String) -> Result<HttpResponse> {
|
||||||
|
let mut sub: Subreddit = subreddit(&sub_name).await;
|
||||||
|
let posts: Vec<Post> = posts(sub_name, &sort).await;
|
||||||
|
|
||||||
|
sub.icon = if sub.icon!="" {format!(r#"<img class="subreddit_icon" src="{}">"#, sub.icon)} else {String::new()};
|
||||||
|
|
||||||
|
let s = SubredditTemplate {
|
||||||
|
sub: sub,
|
||||||
|
posts: posts,
|
||||||
|
sort: sort
|
||||||
|
}
|
||||||
|
.render()
|
||||||
|
.unwrap();
|
||||||
|
Ok(HttpResponse::Ok().content_type("text/html").body(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SERVICES
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[get("/r/{sub}")]
|
||||||
|
async fn page(web::Path(sub): web::Path<String>) -> Result<HttpResponse> {
|
||||||
|
render(sub, String::from("hot")).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[get("/r/{sub}/{sort}")]
|
||||||
|
async fn sorted(web::Path((sub, sort)): web::Path<(String, String)>) -> Result<HttpResponse> {
|
||||||
|
render(sub, sort).await
|
||||||
|
}
|
||||||
|
|
||||||
|
// UTILITIES
|
||||||
|
async fn val (j: &serde_json::Value, k: &str) -> String { String::from(j["data"][k].as_str().unwrap_or("")) }
|
||||||
|
|
||||||
|
// SUBREDDIT
|
||||||
|
async fn subreddit(sub: &String) -> Subreddit {
|
||||||
|
let url: String = format!("https://www.reddit.com/r/{}/about.json", sub);
|
||||||
|
let resp: String = reqwest::get(&url).await.unwrap().text().await.unwrap();
|
||||||
|
|
||||||
|
let data: serde_json::Value = serde_json::from_str(resp.as_str()).expect("Failed to parse JSON");
|
||||||
|
|
||||||
|
let icon: String = String::from(data["data"]["community_icon"].as_str().unwrap()); //val(&data, "community_icon");
|
||||||
|
let icon_split: std::str::Split<&str> = icon.split("?");
|
||||||
|
let icon_parts: Vec<&str> = icon_split.collect();
|
||||||
|
|
||||||
|
Subreddit {
|
||||||
|
name: val(&data, "display_name").await,
|
||||||
|
title: val(&data, "title").await,
|
||||||
|
description: val(&data, "public_description").await,
|
||||||
|
icon: String::from(icon_parts[0]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// POSTS
|
||||||
|
pub async fn posts(sub: String, sort: &String) -> Vec<Post> {
|
||||||
|
let url: String = format!("https://www.reddit.com/r/{}/{}.json", sub, sort);
|
||||||
|
let resp: String = reqwest::get(&url).await.unwrap().text().await.unwrap();
|
||||||
|
|
||||||
|
let popular: serde_json::Value = serde_json::from_str(resp.as_str()).expect("Failed to parse JSON");
|
||||||
|
let post_list = popular["data"]["children"].as_array().unwrap();
|
||||||
|
|
||||||
|
let mut posts: Vec<Post> = Vec::new();
|
||||||
|
|
||||||
|
for post in post_list.iter() {
|
||||||
|
let img = if val(post, "thumbnail").await.starts_with("https:/") { val(post, "thumbnail").await } else { String::new() };
|
||||||
|
let unix_time: i64 = post["data"]["created_utc"].as_f64().unwrap().round() as i64;
|
||||||
|
let score = post["data"]["score"].as_i64().unwrap();
|
||||||
|
posts.push(Post {
|
||||||
|
title: val(post, "title").await,
|
||||||
|
community: val(post, "subreddit").await,
|
||||||
|
author: val(post, "author").await,
|
||||||
|
score: if score>1000 {format!("{}k",score/1000)} else {score.to_string()},
|
||||||
|
image: img,
|
||||||
|
url: val(post, "permalink").await,
|
||||||
|
time: Utc.timestamp(unix_time, 0).format("%b %e '%y").to_string()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
posts
|
||||||
|
}
|
106
src/user.rs
Normal file
106
src/user.rs
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
// CRATES
|
||||||
|
extern crate comrak;
|
||||||
|
use actix_web::{get, web, HttpResponse, Result};
|
||||||
|
use askama::Template;
|
||||||
|
use chrono::{TimeZone, Utc};
|
||||||
|
|
||||||
|
// STRUCTS
|
||||||
|
#[derive(Template)]
|
||||||
|
#[template(path = "user.html", escape = "none")]
|
||||||
|
struct UserTemplate {
|
||||||
|
user: User,
|
||||||
|
posts: Vec<Post>,
|
||||||
|
sort: String
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Post {
|
||||||
|
pub title: String,
|
||||||
|
pub community: String,
|
||||||
|
pub author: String,
|
||||||
|
pub score: String,
|
||||||
|
pub image: String,
|
||||||
|
pub url: String,
|
||||||
|
pub time: String
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct User {
|
||||||
|
pub name: String,
|
||||||
|
pub icon: String,
|
||||||
|
pub karma: i64,
|
||||||
|
pub banner: String,
|
||||||
|
pub description: String
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async fn render(username: String, sort: String) -> Result<HttpResponse> {
|
||||||
|
let user: User = user(&username).await;
|
||||||
|
let posts: Vec<Post> = posts(username, &sort).await;
|
||||||
|
|
||||||
|
let s = UserTemplate {
|
||||||
|
user: user,
|
||||||
|
posts: posts,
|
||||||
|
sort: sort
|
||||||
|
}
|
||||||
|
.render()
|
||||||
|
.unwrap();
|
||||||
|
Ok(HttpResponse::Ok().content_type("text/html").body(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SERVICES
|
||||||
|
#[get("/u/{username}")]
|
||||||
|
async fn page(web::Path(username): web::Path<String>) -> Result<HttpResponse> {
|
||||||
|
render(username, "hot".to_string()).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/u/{username}/{sort}")]
|
||||||
|
async fn sorted(web::Path((username, sort)): web::Path<(String, String)>) -> Result<HttpResponse> {
|
||||||
|
render(username, sort).await
|
||||||
|
}
|
||||||
|
|
||||||
|
// UTILITIES
|
||||||
|
async fn user_val (j: &serde_json::Value, k: &str) -> String { String::from(j["data"]["subreddit"][k].as_str().unwrap()) }
|
||||||
|
async fn post_val (j: &serde_json::Value, k: &str) -> String { String::from(j["data"][k].as_str().unwrap_or("Comment")) }
|
||||||
|
|
||||||
|
// USER
|
||||||
|
async fn user(name: &String) -> User {
|
||||||
|
let url: String = format!("https://www.reddit.com/user/{}/about.json", name);
|
||||||
|
let resp: String = reqwest::get(&url).await.unwrap().text().await.unwrap();
|
||||||
|
|
||||||
|
let data: serde_json::Value = serde_json::from_str(resp.as_str()).expect("Failed to parse JSON");
|
||||||
|
|
||||||
|
User {
|
||||||
|
name: name.to_string(),
|
||||||
|
icon: user_val(&data, "icon_img").await,
|
||||||
|
karma: data["data"]["total_karma"].as_i64().unwrap(),
|
||||||
|
banner: user_val(&data, "banner_img").await,
|
||||||
|
description: user_val(&data, "public_description").await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// POSTS
|
||||||
|
async fn posts(sub: String, sort: &String) -> Vec<Post> {
|
||||||
|
let url: String = format!("https://www.reddit.com/u/{}/.json?sort={}", sub, sort);
|
||||||
|
let resp: String = reqwest::get(&url).await.unwrap().text().await.unwrap();
|
||||||
|
|
||||||
|
let popular: serde_json::Value = serde_json::from_str(resp.as_str()).expect("Failed to parse JSON");
|
||||||
|
let post_list = popular["data"]["children"].as_array().unwrap();
|
||||||
|
|
||||||
|
let mut posts: Vec<Post> = Vec::new();
|
||||||
|
|
||||||
|
for post in post_list.iter() {
|
||||||
|
let img = if post_val(post, "thumbnail").await.starts_with("https:/") { post_val(post, "thumbnail").await } else { String::new() };
|
||||||
|
let unix_time: i64 = post["data"]["created_utc"].as_f64().unwrap().round() as i64;
|
||||||
|
let score = post["data"]["score"].as_i64().unwrap();
|
||||||
|
posts.push(Post {
|
||||||
|
title: post_val(post, "title").await,
|
||||||
|
community: post_val(post, "subreddit").await,
|
||||||
|
author: post_val(post, "author").await,
|
||||||
|
score: if score>1000 {format!("{}k",score/1000)} else {score.to_string()},
|
||||||
|
image: img,
|
||||||
|
url: post_val(post, "permalink").await,
|
||||||
|
time: Utc.timestamp(unix_time, 0).format("%b %e '%y").to_string()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
posts
|
||||||
|
}
|
305
static/style.css
Normal file
305
static/style.css
Normal file
@ -0,0 +1,305 @@
|
|||||||
|
/* General */
|
||||||
|
|
||||||
|
* {
|
||||||
|
transition: 0.2s all;
|
||||||
|
margin: 0px;
|
||||||
|
color: white;
|
||||||
|
font-family: sans-serif;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
background: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
color: aqua;
|
||||||
|
background: #151515;
|
||||||
|
padding: 15px;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
max-width: 750px;
|
||||||
|
margin: 0 auto;
|
||||||
|
margin-top: 25px;
|
||||||
|
padding: 0px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:not(.post_right):hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
color: aqua;
|
||||||
|
}
|
||||||
|
|
||||||
|
#about {
|
||||||
|
background: #151515;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Subreddit */
|
||||||
|
|
||||||
|
.subreddit {
|
||||||
|
max-width: 750px;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: flex;
|
||||||
|
padding-bottom: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subreddit_name {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subreddit_right {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: column;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subreddit_icon {
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
border-radius: 100%;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* User */
|
||||||
|
|
||||||
|
.user {
|
||||||
|
max-width: 750px;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user_right {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: column;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user_icon {
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
border-radius: 100%;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sorting */
|
||||||
|
|
||||||
|
#sort {
|
||||||
|
max-width: 750px;
|
||||||
|
margin: 20px -10px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: start;
|
||||||
|
padding: 0px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sort > div {
|
||||||
|
background: #151515;
|
||||||
|
color: lightgrey;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-right: 5px;
|
||||||
|
padding: 10px 20px;
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sort > div:hover {
|
||||||
|
background: #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Post */
|
||||||
|
|
||||||
|
.post {
|
||||||
|
border-radius: 5px;
|
||||||
|
background: #151515;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post:hover {
|
||||||
|
background: #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post:hover > .post_left {
|
||||||
|
background: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post_left, .post_right {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post_left {
|
||||||
|
text-align: center;
|
||||||
|
background: #222;
|
||||||
|
border-radius: 5px 0px 0px 5px;
|
||||||
|
min-width: 50px;
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post_title {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post_upvote {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post_subreddit {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post_score {
|
||||||
|
margin-top: 1em;
|
||||||
|
color: aqua;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post_right {
|
||||||
|
padding: 20px 25px;
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-shrink: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post_right > * {
|
||||||
|
margin: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post_right > p {
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post_image {
|
||||||
|
max-width: 500px;
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post_image[src=""] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post_body {
|
||||||
|
opacity: 0.9;
|
||||||
|
font-weight: normal;
|
||||||
|
margin: 10px 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post_body > p:not(:first-child) {
|
||||||
|
margin-top: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post_body a {
|
||||||
|
text-decoration: underline;
|
||||||
|
color: aqua;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post_thumbnail {
|
||||||
|
object-fit: cover;
|
||||||
|
width: auto;
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post_thumbnail[src=""] {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Comment */
|
||||||
|
|
||||||
|
.comment {
|
||||||
|
border-radius: 5px;
|
||||||
|
background: #151515;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment:hover {
|
||||||
|
background: #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment:hover > .comment_left {
|
||||||
|
background: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment_left, .comment_right {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment_left {
|
||||||
|
text-align: center;
|
||||||
|
background: #222;
|
||||||
|
border-radius: 5px 0px 0px 5px;
|
||||||
|
min-width: 50px;
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment_title {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment_upvote {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment_subreddit {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment_score {
|
||||||
|
margin-top: 1em;
|
||||||
|
color: aqua;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment_right {
|
||||||
|
padding: 20px 25px;
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-shrink: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment_right > * {
|
||||||
|
margin: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment_right > p {
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment_image {
|
||||||
|
max-width: 500px;
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment_image[src=""] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment_body {
|
||||||
|
opacity: 0.9;
|
||||||
|
font-weight: normal;
|
||||||
|
margin: 10px 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment_body > p:not(:first-child) {
|
||||||
|
margin-top: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment_body a {
|
||||||
|
text-decoration: underline;
|
||||||
|
color: aqua;
|
||||||
|
}
|
40
templates/popular.html
Normal file
40
templates/popular.html
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Libreddit</title>
|
||||||
|
<meta name="description" content="Alternative private front-end to Reddit">
|
||||||
|
<link rel="stylesheet" href="/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<a href="/"><span style="color:white">lib</span>reddit.</a>
|
||||||
|
<a style="color:white" href="https://github.com/spikecodes/libreddit">GITHUB</a>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
<div id="sort">
|
||||||
|
<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>
|
||||||
|
{% 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>
|
||||||
|
<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>
|
||||||
|
<h3 class="post_title"><a href="{{ post.url }}">{{ post.title }}</a></h3>
|
||||||
|
</div>
|
||||||
|
<img class="post_thumbnail" src="{{ post.image }}">
|
||||||
|
</div><br>
|
||||||
|
{% endfor %}
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
64
templates/post.html
Normal file
64
templates/post.html
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>{{ post.title }} - r/{{ post.community }}</title>
|
||||||
|
<meta name="author" content="u/{{ post.author }}">
|
||||||
|
<meta name="description" content="View on Libreddit, an alternative private front-end to Reddit.">
|
||||||
|
<link rel="stylesheet" href="/style.css">
|
||||||
|
<style>
|
||||||
|
/* #sort > #sort_{{ sort }} {
|
||||||
|
background: aqua;
|
||||||
|
color: black;
|
||||||
|
} */
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<a href="/"><span style="color:white">lib</span>reddit.</a>
|
||||||
|
<a style="color:white" href="https://github.com/spikecodes/libreddit">GITHUB</a>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
<div class="post" style="border: 2px solid #555;background: #222;">
|
||||||
|
<div class="post_left" style="background: #333;">
|
||||||
|
<button class="post_upvote">↑</button>
|
||||||
|
<h3 class="post_score">{{ post.score }}</h3>
|
||||||
|
<button class="post_upvote">↓</button>
|
||||||
|
</div>
|
||||||
|
<div class="post_right">
|
||||||
|
<p>
|
||||||
|
<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>
|
||||||
|
<h3 class="post_title">{{ post.title }}</h3>
|
||||||
|
{{ post.media }}
|
||||||
|
<h4 class="post_body">{{ post.body }}</h4>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="sort">
|
||||||
|
<div id="sort_confidence"><a href="{{ post.url }}confidence">Best</a></div>
|
||||||
|
<div id="sort_top"><a href="{{ post.url }}top">Top</a></div>
|
||||||
|
<div id="sort_new"><a href="{{ post.url }}new">New</a></div>
|
||||||
|
<div id="sort_controversial"><a href="{{ post.url }}controversial">Controversial</a></div>
|
||||||
|
<div id="sort_old"><a href="{{ post.url }}old">Old</a></div>
|
||||||
|
</div>
|
||||||
|
{% for comment in comments %}
|
||||||
|
<div class="comment">
|
||||||
|
<div class="comment_left">
|
||||||
|
<button class="comment_upvote">↑</button>
|
||||||
|
<h3 class="comment_score">{{ comment.score }}</h3>
|
||||||
|
<button class="comment_upvote">↓</button>
|
||||||
|
</div>
|
||||||
|
<div class="comment_right">
|
||||||
|
<p>
|
||||||
|
Posted by <a class="comment_author" href="/u/{{ comment.author }}">u/{{ comment.author }}</a>
|
||||||
|
<span style="float: right;">{{ comment.time }}</span>
|
||||||
|
</p>
|
||||||
|
<h4 class="comment_body">{{ comment.body }}</h4>
|
||||||
|
</div>
|
||||||
|
</div><br>
|
||||||
|
{% endfor %}
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
51
templates/subreddit.html
Normal file
51
templates/subreddit.html
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>r/{{ sub.name }}: {{ sub.description }}</title>
|
||||||
|
<meta name="description" content="View on Libreddit, an alternative private front-end to Reddit.">
|
||||||
|
<link rel="stylesheet" href="/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<a href="/"><span style="color:white">lib</span>reddit.</a>
|
||||||
|
<a style="color:white" href="https://github.com/spikecodes/libreddit">GITHUB</a>
|
||||||
|
</header>
|
||||||
|
<div id="about">
|
||||||
|
<div class="subreddit">
|
||||||
|
<div class="subreddit_left">
|
||||||
|
{{ sub.icon }}
|
||||||
|
</div>
|
||||||
|
<div class="subreddit_right">
|
||||||
|
<h2 class="subreddit_name">r/{{ sub.name }}</h2>
|
||||||
|
<p class="subreddit_description">{{ sub.description }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<main>
|
||||||
|
<div id="sort">
|
||||||
|
<div id="sort_hot"><a href="/r/{{ sub.name }}/hot">Hot</a></div>
|
||||||
|
<div id="sort_top"><a href="/r/{{ sub.name }}/top">Top</a></div>
|
||||||
|
<div id="sort_new"><a href="/r/{{ sub.name }}/new">New</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>
|
||||||
|
<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 style="float: right;">{{ post.time }}</span>
|
||||||
|
</p>
|
||||||
|
<h3 class="post_title"><a href="{{ post.url }}">{{ post.title }}</a></h3>
|
||||||
|
</div>
|
||||||
|
<img class="post_thumbnail" src="{{ post.image }}">
|
||||||
|
</div><br>
|
||||||
|
{% endfor %}
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
51
templates/user.html
Normal file
51
templates/user.html
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Libreddit: u/{{ user.name }}</title>
|
||||||
|
<meta name="description" content="View on Libreddit, an alternative private front-end to Reddit.">
|
||||||
|
<link rel="stylesheet" href="/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<a href="/"><span style="color:white">lib</span>reddit.</a>
|
||||||
|
<a style="color:white" href="https://github.com/spikecodes/libreddit">GITHUB</a>
|
||||||
|
</header>
|
||||||
|
<div id="about">
|
||||||
|
<div class="user">
|
||||||
|
<div class="user_left">
|
||||||
|
<img class="user_icon" src="{{ user.icon }}">
|
||||||
|
</div>
|
||||||
|
<div class="user_right">
|
||||||
|
<h2 class="user_name">u/{{ user.name }}</h2>
|
||||||
|
<p class="user_description"><span>Karma:</span> {{ user.karma }} | <span>Description:</span> "{{ user.description }}"</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<main>
|
||||||
|
<div id="sort">
|
||||||
|
<div id="sort_hot"><a href="/u/{{ user.name }}/?sort=hot">Hot</a></div>
|
||||||
|
<div id="sort_top"><a href="/u/{{ user.name }}/?sort=top">Top</a></div>
|
||||||
|
<div id="sort_new"><a href="/u/{{ user.name }}/?sort=new">New</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>
|
||||||
|
<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>
|
||||||
|
<h3 class="post_title"><a href="{{ post.url }}">{{ post.title }}</a></h3>
|
||||||
|
</div>
|
||||||
|
<img class="post_thumbnail" src="{{ post.image }}">
|
||||||
|
</div><br>
|
||||||
|
{% endfor %}
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
Reference in New Issue
Block a user