Add Wiki Pages

This commit is contained in:
spikecodes 2021-01-01 22:21:43 -08:00
parent 2f2ed6169d
commit 0925a9b334
10 changed files with 161 additions and 66 deletions

View File

@ -58,11 +58,12 @@ async fn main() -> std::io::Result<()> {
// USER SERVICES // USER SERVICES
.route("/u/{username}/", web::get().to(user::profile)) .route("/u/{username}/", web::get().to(user::profile))
.route("/user/{username}/", web::get().to(user::profile)) .route("/user/{username}/", web::get().to(user::profile))
// WIKI SERVICES
.route("/r/{sub}/wiki/", web::get().to(subreddit::wiki))
.route("/r/{sub}/wiki/{page}/", web::get().to(subreddit::wiki))
// SUBREDDIT SERVICES // SUBREDDIT SERVICES
.route("/r/{sub}/", web::get().to(subreddit::page)) .route("/r/{sub}/", web::get().to(subreddit::page))
.route("/r/{sub}/{sort}/", web::get().to(subreddit::page)) .route("/r/{sub}/{sort:hot|new|top|rising}/", web::get().to(subreddit::page))
// WIKI SERVICES
// .route("/r/{sub}/wiki/index", web::get().to(subreddit::wiki))
// POPULAR SERVICES // POPULAR SERVICES
.route("/", web::get().to(subreddit::page)) .route("/", web::get().to(subreddit::page))
.route("/{sort:best|hot|new|top|rising}/", web::get().to(subreddit::page)) .route("/{sort:best|hot|new|top|rising}/", web::get().to(subreddit::page))

View File

@ -24,7 +24,7 @@ pub async fn item(req: HttpRequest) -> Result<HttpResponse> {
// Log the post ID being fetched in debug mode // Log the post ID being fetched in debug mode
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
dbg!(&id); dbg!(&id);
// Send a request to the url, receive JSON in response // Send a request to the url, receive JSON in response
match request(&path).await { match request(&path).await {
// Otherwise, grab the JSON output from the request // Otherwise, grab the JSON output from the request
@ -36,9 +36,9 @@ pub async fn item(req: HttpRequest) -> Result<HttpResponse> {
// Use the Post and Comment structs to generate a website to show users // Use the Post and Comment structs to generate a website to show users
let s = PostTemplate { comments, post, sort }.render().unwrap(); let s = PostTemplate { comments, post, sort }.render().unwrap();
Ok(HttpResponse::Ok().content_type("text/html").body(s)) Ok(HttpResponse::Ok().content_type("text/html").body(s))
}, }
// If the Reddit API returns an error, exit and send error page to user // If the Reddit API returns an error, exit and send error page to user
Err(msg) => error(msg.to_string()).await Err(msg) => error(msg.to_string()).await,
} }
} }

View File

@ -38,7 +38,7 @@ pub async fn find(req: HttpRequest) -> Result<HttpResponse> {
.render() .render()
.unwrap(); .unwrap();
Ok(HttpResponse::Ok().content_type("text/html").body(s)) Ok(HttpResponse::Ok().content_type("text/html").body(s))
}, }
Err(msg) => error(msg.to_string()).await Err(msg) => error(msg.to_string()).await,
} }
} }

View File

@ -13,8 +13,14 @@ struct SubredditTemplate {
ends: (String, String), ends: (String, String),
} }
#[derive(Template)]
#[template(path = "wiki.html", escape = "none")]
struct WikiTemplate {
sub: String,
wiki: String,
}
// SERVICES // SERVICES
// web::Path(sub): web::Path<String>, params: web::Query<Params>
pub async fn page(req: HttpRequest) -> Result<HttpResponse> { pub async fn page(req: HttpRequest) -> Result<HttpResponse> {
let path = format!("{}.json?{}", req.path(), req.query_string()); let path = format!("{}.json?{}", req.path(), req.query_string());
let sub = req.match_info().get("sub").unwrap_or("popular").to_string(); let sub = req.match_info().get("sub").unwrap_or("popular").to_string();
@ -37,8 +43,27 @@ pub async fn page(req: HttpRequest) -> Result<HttpResponse> {
.render() .render()
.unwrap(); .unwrap();
Ok(HttpResponse::Ok().content_type("text/html").body(s)) Ok(HttpResponse::Ok().content_type("text/html").body(s))
}, }
Err(msg) => error(msg.to_string()).await Err(msg) => error(msg.to_string()).await,
}
}
pub async fn wiki(req: HttpRequest) -> Result<HttpResponse> {
let sub = req.match_info().get("sub").unwrap();
let page = req.match_info().get("page").unwrap_or("index");
let path: String = format!("r/{}/wiki/{}.json?raw_json=1", sub, page);
match request(&path).await {
Ok(res) => {
let s = WikiTemplate {
sub: sub.to_string(),
wiki: res["data"]["content_html"].as_str().unwrap().to_string(),
}
.render()
.unwrap();
Ok(HttpResponse::Ok().content_type("text/html").body(s))
}
Err(msg) => error(msg.to_string()).await,
} }
} }
@ -47,33 +72,32 @@ async fn subreddit(sub: &str) -> Result<Subreddit, &'static str> {
// Build the Reddit JSON API url // Build the Reddit JSON API url
let path: String = format!("r/{}/about.json?raw_json=1", sub); let path: String = format!("r/{}/about.json?raw_json=1", sub);
let res;
// Send a request to the url // Send a request to the url
match request(&path).await { match request(&path).await {
// If success, receive JSON in response // If success, receive JSON in response
Ok(response) => { res = response; }, Ok(res) => {
// Metadata regarding the subreddit
let members: i64 = res["data"]["subscribers"].as_u64().unwrap_or_default() as i64;
let active: i64 = res["data"]["accounts_active"].as_u64().unwrap_or_default() as i64;
// Fetch subreddit icon either from the community_icon or icon_img value
let community_icon: &str = res["data"]["community_icon"].as_str().unwrap_or("").split('?').collect::<Vec<&str>>()[0];
let icon = if community_icon.is_empty() { val(&res, "icon_img") } else { community_icon.to_string() };
let sub = Subreddit {
name: val(&res, "display_name"),
title: val(&res, "title"),
description: val(&res, "public_description"),
info: val(&res, "description_html").replace("\\", ""),
icon: format_url(icon).await,
members: format_num(members),
active: format_num(active),
wiki: res["data"]["wiki_enabled"].as_bool().unwrap_or_default(),
};
Ok(sub)
}
// If the Reddit API returns an error, exit this function // If the Reddit API returns an error, exit this function
Err(msg) => return Err(msg) Err(msg) => return Err(msg),
} }
// Metadata regarding the subreddit
let members: i64 = res["data"]["subscribers"].as_u64().unwrap_or_default() as i64;
let active: i64 = res["data"]["accounts_active"].as_u64().unwrap_or_default() as i64;
// Fetch subreddit icon either from the community_icon or icon_img value
let community_icon: &str = res["data"]["community_icon"].as_str().unwrap_or("").split('?').collect::<Vec<&str>>()[0];
let icon = if community_icon.is_empty() { val(&res, "icon_img") } else { community_icon.to_string() };
let sub = Subreddit {
name: val(&res, "display_name"),
title: val(&res, "title"),
description: val(&res, "public_description"),
info: val(&res, "description_html").replace("\\", ""),
icon: format_url(icon).await,
members: format_num(members),
active: format_num(active),
};
Ok(sub)
} }

View File

@ -25,7 +25,7 @@ pub async fn profile(req: HttpRequest) -> Result<HttpResponse> {
// Request user profile data and user posts/comments from Reddit // Request user profile data and user posts/comments from Reddit
let user = user(&username).await; let user = user(&username).await;
let posts = fetch_posts(&path, "Comment".to_string()).await; let posts = fetch_posts(&path, "Comment".to_string()).await;
match posts { match posts {
Ok(items) => { Ok(items) => {
let s = UserTemplate { let s = UserTemplate {
@ -37,9 +37,9 @@ pub async fn profile(req: HttpRequest) -> Result<HttpResponse> {
.render() .render()
.unwrap(); .unwrap();
Ok(HttpResponse::Ok().content_type("text/html").body(s)) Ok(HttpResponse::Ok().content_type("text/html").body(s))
}, }
// If there is an error show error page // If there is an error show error page
Err(msg) => error(msg.to_string()).await Err(msg) => error(msg.to_string()).await,
} }
} }
@ -53,9 +53,11 @@ async fn user(name: &str) -> Result<User, &'static str> {
// Send a request to the url // Send a request to the url
match request(&path).await { match request(&path).await {
// If success, receive JSON in response // If success, receive JSON in response
Ok(response) => { res = response; }, Ok(response) => {
res = response;
}
// If the Reddit API returns an error, exit this function // If the Reddit API returns an error, exit this function
Err(msg) => return Err(msg) Err(msg) => return Err(msg),
} }
// Grab creation date as unix timestamp // Grab creation date as unix timestamp

View File

@ -4,7 +4,7 @@
use actix_web::{http::StatusCode, HttpResponse, Result}; use actix_web::{http::StatusCode, HttpResponse, Result};
use askama::Template; use askama::Template;
use chrono::{TimeZone, Utc}; use chrono::{TimeZone, Utc};
use serde_json::{from_str}; use serde_json::from_str;
use url::Url; use url::Url;
// use surf::{client, get, middleware::Redirect}; // use surf::{client, get, middleware::Redirect};
@ -70,6 +70,7 @@ pub struct Subreddit {
pub icon: String, pub icon: String,
pub members: String, pub members: String,
pub active: String, pub active: String,
pub wiki: bool,
} }
// Parser for query params, used in sorting (eg. /r/rust/?sort=hot) // Parser for query params, used in sorting (eg. /r/rust/?sort=hot)
@ -130,7 +131,7 @@ pub fn format_num(num: i64) -> String {
// val() function used to parse JSON from Reddit APIs // val() function used to parse JSON from Reddit APIs
pub fn val(j: &serde_json::Value, k: &str) -> String { pub fn val(j: &serde_json::Value, k: &str) -> String {
String::from(j["data"][k].as_str().unwrap_or("")) String::from(j["data"][k].as_str().unwrap_or_default())
} }
// nested_val() function used to parse JSON from Reddit APIs // nested_val() function used to parse JSON from Reddit APIs
@ -146,15 +147,17 @@ pub async fn fetch_posts(path: &str, fallback_title: String) -> Result<(Vec<Post
// Send a request to the url // Send a request to the url
match request(&path).await { match request(&path).await {
// If success, receive JSON in response // If success, receive JSON in response
Ok(response) => { res = response; }, Ok(response) => {
res = response;
}
// If the Reddit API returns an error, exit this function // If the Reddit API returns an error, exit this function
Err(msg) => return Err(msg) Err(msg) => return Err(msg),
} }
// Fetch the list of posts from the JSON response // Fetch the list of posts from the JSON response
match res["data"]["children"].as_array() { match res["data"]["children"].as_array() {
Some(list) => { post_list = list }, Some(list) => post_list = list,
None => { return Err("No posts found") } None => return Err("No posts found"),
} }
let mut posts: Vec<Post> = Vec::new(); let mut posts: Vec<Post> = Vec::new();
@ -250,7 +253,7 @@ pub async fn request(path: &str) -> Result<serde_json::Value, &'static str> {
Err("Failed to parse page JSON data") Err("Failed to parse page JSON data")
} }
} }
}, }
false => { false => {
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
dbg!(format!("{} - Page not found", url)); dbg!(format!("{} - Page not found", url));

View File

@ -24,7 +24,6 @@
body { body {
background: var(--background); background: var(--background);
visibility: visible !important;
} }
nav { nav {
@ -49,7 +48,11 @@ main {
margin: 20px auto; margin: 20px auto;
} }
#column_one { max-width: 750px; } #column_one {
max-width: 750px;
border-radius: 5px;
overflow: hidden;
}
footer { footer {
display: flex; display: flex;
@ -100,10 +103,14 @@ aside {
height: max-content; height: max-content;
background: var(--outside); background: var(--outside);
border-radius: 5px; border-radius: 5px;
overflow: hidden;
} }
#user *, #subreddit * { text-align: center; } #user *, #subreddit * { text-align: center; }
#subreddit { padding: 0; }
#sub_meta { padding: 20px; }
#sidebar, #sidebar_contents { margin-top: 20px; } #sidebar, #sidebar_contents { margin-top: 20px; }
#sidebar_label { #sidebar_label {
@ -111,7 +118,7 @@ aside {
padding: 10px; padding: 10px;
} }
#user_icon, #subreddit_icon { #user_icon, #sub_icon {
width: 100px; width: 100px;
height: 100px; height: 100px;
border: 2px solid var(--accent); border: 2px solid var(--accent);
@ -120,30 +127,55 @@ aside {
margin: 10px; margin: 10px;
} }
#user_title, #subreddit_title { #user_title, #sub_title {
margin: 0 20px; margin: 0 20px;
font-size: 20px; font-size: 20px;
font-weight: bold; font-weight: bold;
} }
#user_description, #subreddit_description { #user_description, #sub_description {
margin: 0 20px; margin: 0 20px;
font-size: 15px; font-size: 15px;
} }
#user_name, #subreddit_name, #user_icon, #subreddit_icon, #user_description, #subreddit_description { margin-bottom: 20px; } #user_name, #sub_name, #user_icon, #sub_icon, #user_description, #sub_description { margin-bottom: 20px; }
#user_details, #subreddit_details { #user_details, #sub_details {
display: grid; display: grid;
grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(2, 1fr);
grid-column-gap: 20px; grid-column-gap: 20px;
} }
#user_details > label, #subreddit_details > label { #user_details > label, #sub_details > label {
color: var(--accent); color: var(--accent);
font-size: 15px; font-size: 15px;
} }
/* Wiki Pages */
#wiki {
background: var(--foreground);
padding: 35px;
}
#top {
background: var(--highlighted);
font-size: 18px;
width: 100%;
display: flex;
}
#top > * {
flex-grow: 1;
text-align: center;
height: 40px;
line-height: 40px;
}
#top > div {
border-bottom: 2px solid white;
}
/* Sorting and Search */ /* Sorting and Search */
select { select {
@ -500,11 +532,11 @@ input[type="submit"]:hover { color: var(--accent); }
} }
.md a { .md a {
text-decoration: underline;
color: var(--accent); color: var(--accent);
} }
.md li { margin: 10px 0; } .md li { margin: 10px 0; }
.toc_child { list-style: none; }
.md pre { .md pre {
background: var(--outside); background: var(--outside);

View File

@ -12,7 +12,7 @@
<link rel="stylesheet" href="/style.css"> <link rel="stylesheet" href="/style.css">
{% endblock %} {% endblock %}
</head> </head>
<body style="visibility: hidden;"> <body>
<nav> <nav>
<a id="logo" href="/"><span id="lib">lib</span>reddit. <span id="version">v{{ env!("CARGO_PKG_VERSION") }}</span></a> <a id="logo" href="/"><span id="lib">lib</span>reddit. <span id="version">v{{ env!("CARGO_PKG_VERSION") }}</span></a>
{% block search %}{% endblock %} {% block search %}{% endblock %}

View File

@ -66,15 +66,23 @@
{% if sub.name != "" %} {% if sub.name != "" %}
<aside> <aside>
<div id="subreddit"> <div id="subreddit">
<img id="subreddit_icon" src="{{ sub.icon }}"> {% if sub.wiki %}
<p id="subreddit_title">{{ sub.title }}</p> <div id="top">
<p id="subreddit_name">r/{{ sub.name }}</p> <div>Posts</div>
<p id="subreddit_description">{{ sub.description }}</p> <a href="/r/{{ sub.name }}/wiki/index">Wiki</a>
<div id="subreddit_details"> </div>
<label>Members</label> {% endif %}
<label>Active</label> <div id="sub_meta">
<div>{{ sub.members }}</div> <img id="sub_icon" src="{{ sub.icon }}">
<div>{{ sub.active }}</div> <p id="sub_title">{{ sub.title }}</p>
<p id="sub_name">r/{{ sub.name }}</p>
<p id="sub_description">{{ sub.description }}</p>
<div id="sub_details">
<label>Members</label>
<label>Active</label>
<div>{{ sub.members }}</div>
<div>{{ sub.active }}</div>
</div>
</div> </div>
</div> </div>
<details id="sidebar"> <details id="sidebar">

25
templates/wiki.html Normal file
View File

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