Merge pull request #771 from gmnsii/comment-search
This commit is contained in:
commit
aaf05de1a8
6 changed files with 185 additions and 101 deletions
4
build.rs
4
build.rs
|
@ -1,6 +1,4 @@
|
||||||
use std::{
|
use std::process::{Command, ExitStatus, Output};
|
||||||
process::{Command, ExitStatus, Output},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
#[cfg(not(target_os = "windows"))]
|
||||||
use std::os::unix::process::ExitStatusExt;
|
use std::os::unix::process::ExitStatusExt;
|
||||||
|
|
229
src/post.rs
229
src/post.rs
|
@ -8,6 +8,8 @@ use crate::utils::{
|
||||||
use hyper::{Body, Request, Response};
|
use hyper::{Body, Request, Response};
|
||||||
|
|
||||||
use askama::Template;
|
use askama::Template;
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
use regex::Regex;
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
// STRUCTS
|
// STRUCTS
|
||||||
|
@ -20,13 +22,18 @@ struct PostTemplate {
|
||||||
prefs: Preferences,
|
prefs: Preferences,
|
||||||
single_thread: bool,
|
single_thread: bool,
|
||||||
url: String,
|
url: String,
|
||||||
|
url_without_query: String,
|
||||||
|
comment_query: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static COMMENT_SEARCH_CAPTURE: Lazy<Regex> = Lazy::new(|| Regex::new(r#"\?q=(.*)&type=comment"#).unwrap());
|
||||||
|
|
||||||
pub async fn item(req: Request<Body>) -> Result<Response<Body>, String> {
|
pub async fn item(req: Request<Body>) -> Result<Response<Body>, String> {
|
||||||
// Build Reddit API path
|
// Build Reddit API path
|
||||||
let mut path: String = format!("{}.json?{}&raw_json=1", req.uri().path(), req.uri().query().unwrap_or_default());
|
let mut path: String = format!("{}.json?{}&raw_json=1", req.uri().path(), req.uri().query().unwrap_or_default());
|
||||||
let sub = req.param("sub").unwrap_or_default();
|
let sub = req.param("sub").unwrap_or_default();
|
||||||
let quarantined = can_access_quarantine(&req, &sub);
|
let quarantined = can_access_quarantine(&req, &sub);
|
||||||
|
let url = req.uri().to_string();
|
||||||
|
|
||||||
// Set sort to sort query parameter
|
// Set sort to sort query parameter
|
||||||
let sort = param(&path, "sort").unwrap_or_else(|| {
|
let sort = param(&path, "sort").unwrap_or_else(|| {
|
||||||
|
@ -63,17 +70,26 @@ pub async fn item(req: Request<Body>) -> Result<Response<Body>, String> {
|
||||||
return Ok(nsfw_landing(req).await.unwrap_or_default());
|
return Ok(nsfw_landing(req).await.unwrap_or_default());
|
||||||
}
|
}
|
||||||
|
|
||||||
let comments = parse_comments(&response[1], &post.permalink, &post.author.name, highlighted_comment, &get_filters(&req), &req);
|
let query = match COMMENT_SEARCH_CAPTURE.captures(&url) {
|
||||||
let url = req.uri().to_string();
|
Some(captures) => captures.get(1).unwrap().as_str().replace("%20", " ").replace("+", " "),
|
||||||
|
None => String::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let comments = match query.as_str() {
|
||||||
|
"" => parse_comments(&response[1], &post.permalink, &post.author.name, highlighted_comment, &get_filters(&req), &req),
|
||||||
|
_ => query_comments(&response[1], &post.permalink, &post.author.name, highlighted_comment, &get_filters(&req), &query, &req),
|
||||||
|
};
|
||||||
|
|
||||||
// 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
|
||||||
template(PostTemplate {
|
template(PostTemplate {
|
||||||
comments,
|
comments,
|
||||||
post,
|
post,
|
||||||
|
url_without_query: url.clone().trim_end_matches(&format!("?q={query}&type=comment")).to_string(),
|
||||||
sort,
|
sort,
|
||||||
prefs: Preferences::new(&req),
|
prefs: Preferences::new(&req),
|
||||||
single_thread,
|
single_thread,
|
||||||
url,
|
url,
|
||||||
|
comment_query: query,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
// 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
|
||||||
|
@ -89,6 +105,7 @@ pub async fn item(req: Request<Body>) -> Result<Response<Body>, String> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// COMMENTS
|
// COMMENTS
|
||||||
|
|
||||||
fn parse_comments(json: &serde_json::Value, post_link: &str, post_author: &str, highlighted_comment: &str, filters: &HashSet<String>, req: &Request<Body>) -> Vec<Comment> {
|
fn parse_comments(json: &serde_json::Value, post_link: &str, post_author: &str, highlighted_comment: &str, filters: &HashSet<String>, req: &Request<Body>) -> Vec<Comment> {
|
||||||
// Parse the comment JSON into a Vector of Comments
|
// Parse the comment JSON into a Vector of Comments
|
||||||
let comments = json["data"]["children"].as_array().map_or(Vec::new(), std::borrow::ToOwned::to_owned);
|
let comments = json["data"]["children"].as_array().map_or(Vec::new(), std::borrow::ToOwned::to_owned);
|
||||||
|
@ -97,96 +114,136 @@ fn parse_comments(json: &serde_json::Value, post_link: &str, post_author: &str,
|
||||||
comments
|
comments
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|comment| {
|
.map(|comment| {
|
||||||
let kind = comment["kind"].as_str().unwrap_or_default().to_string();
|
|
||||||
let data = &comment["data"];
|
let data = &comment["data"];
|
||||||
|
|
||||||
let unix_time = data["created_utc"].as_f64().unwrap_or_default();
|
|
||||||
let (rel_time, created) = time(unix_time);
|
|
||||||
|
|
||||||
let edited = data["edited"].as_f64().map_or((String::new(), String::new()), time);
|
|
||||||
|
|
||||||
let score = data["score"].as_i64().unwrap_or(0);
|
|
||||||
|
|
||||||
// The JSON API only provides comments up to some threshold.
|
|
||||||
// Further comments have to be loaded by subsequent requests.
|
|
||||||
// The "kind" value will be "more" and the "count"
|
|
||||||
// shows how many more (sub-)comments exist in the respective nesting level.
|
|
||||||
// Note that in certain (seemingly random) cases, the count is simply wrong.
|
|
||||||
let more_count = data["count"].as_i64().unwrap_or_default();
|
|
||||||
|
|
||||||
// If this comment contains replies, handle those too
|
|
||||||
let replies: Vec<Comment> = if data["replies"].is_object() {
|
let replies: Vec<Comment> = if data["replies"].is_object() {
|
||||||
parse_comments(&data["replies"], post_link, post_author, highlighted_comment, filters, req)
|
parse_comments(&data["replies"], post_link, post_author, highlighted_comment, filters, req)
|
||||||
} else {
|
} else {
|
||||||
Vec::new()
|
Vec::new()
|
||||||
};
|
};
|
||||||
|
build_comment(&comment, data, replies, post_link, post_author, highlighted_comment, filters, req)
|
||||||
let awards: Awards = Awards::parse(&data["all_awardings"]);
|
|
||||||
|
|
||||||
let parent_kind_and_id = val(&comment, "parent_id");
|
|
||||||
let parent_info = parent_kind_and_id.split('_').collect::<Vec<&str>>();
|
|
||||||
|
|
||||||
let id = val(&comment, "id");
|
|
||||||
let highlighted = id == highlighted_comment;
|
|
||||||
|
|
||||||
let body = if (val(&comment, "author") == "[deleted]" && val(&comment, "body") == "[removed]") || val(&comment, "body") == "[ Removed by Reddit ]" {
|
|
||||||
format!(
|
|
||||||
"<div class=\"md\"><p>[removed] — <a href=\"https://www.unddit.com{}{}\">view removed comment</a></p></div>",
|
|
||||||
post_link, id
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
rewrite_urls(&val(&comment, "body_html"))
|
|
||||||
};
|
|
||||||
|
|
||||||
let author = Author {
|
|
||||||
name: val(&comment, "author"),
|
|
||||||
flair: Flair {
|
|
||||||
flair_parts: FlairPart::parse(
|
|
||||||
data["author_flair_type"].as_str().unwrap_or_default(),
|
|
||||||
data["author_flair_richtext"].as_array(),
|
|
||||||
data["author_flair_text"].as_str(),
|
|
||||||
),
|
|
||||||
text: val(&comment, "link_flair_text"),
|
|
||||||
background_color: val(&comment, "author_flair_background_color"),
|
|
||||||
foreground_color: val(&comment, "author_flair_text_color"),
|
|
||||||
},
|
|
||||||
distinguished: val(&comment, "distinguished"),
|
|
||||||
};
|
|
||||||
let is_filtered = filters.contains(&["u_", author.name.as_str()].concat());
|
|
||||||
|
|
||||||
// Many subreddits have a default comment posted about the sub's rules etc.
|
|
||||||
// Many libreddit users do not wish to see this kind of comment by default.
|
|
||||||
// Reddit does not tell us which users are "bots", so a good heuristic is to
|
|
||||||
// collapse stickied moderator comments.
|
|
||||||
let is_moderator_comment = data["distinguished"].as_str().unwrap_or_default() == "moderator";
|
|
||||||
let is_stickied = data["stickied"].as_bool().unwrap_or_default();
|
|
||||||
let collapsed = (is_moderator_comment && is_stickied) || is_filtered;
|
|
||||||
|
|
||||||
Comment {
|
|
||||||
id,
|
|
||||||
kind,
|
|
||||||
parent_id: parent_info[1].to_string(),
|
|
||||||
parent_kind: parent_info[0].to_string(),
|
|
||||||
post_link: post_link.to_string(),
|
|
||||||
post_author: post_author.to_string(),
|
|
||||||
body,
|
|
||||||
author,
|
|
||||||
score: if data["score_hidden"].as_bool().unwrap_or_default() {
|
|
||||||
("\u{2022}".to_string(), "Hidden".to_string())
|
|
||||||
} else {
|
|
||||||
format_num(score)
|
|
||||||
},
|
|
||||||
rel_time,
|
|
||||||
created,
|
|
||||||
edited,
|
|
||||||
replies,
|
|
||||||
highlighted,
|
|
||||||
awards,
|
|
||||||
collapsed,
|
|
||||||
is_filtered,
|
|
||||||
more_count,
|
|
||||||
prefs: Preferences::new(req),
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn query_comments(
|
||||||
|
json: &serde_json::Value,
|
||||||
|
post_link: &str,
|
||||||
|
post_author: &str,
|
||||||
|
highlighted_comment: &str,
|
||||||
|
filters: &HashSet<String>,
|
||||||
|
query: &str,
|
||||||
|
req: &Request<Body>,
|
||||||
|
) -> Vec<Comment> {
|
||||||
|
let comments = json["data"]["children"].as_array().map_or(Vec::new(), std::borrow::ToOwned::to_owned);
|
||||||
|
let mut results = Vec::new();
|
||||||
|
|
||||||
|
comments.into_iter().for_each(|comment| {
|
||||||
|
let data = &comment["data"];
|
||||||
|
|
||||||
|
// If this comment contains replies, handle those too
|
||||||
|
if data["replies"].is_object() {
|
||||||
|
results.append(&mut query_comments(&data["replies"], post_link, post_author, highlighted_comment, filters, query, req))
|
||||||
|
}
|
||||||
|
|
||||||
|
let c = build_comment(&comment, data, Vec::new(), post_link, post_author, highlighted_comment, filters, req);
|
||||||
|
if c.body.to_lowercase().contains(&query.to_lowercase()) {
|
||||||
|
results.push(c);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
results
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_comment(
|
||||||
|
comment: &serde_json::Value,
|
||||||
|
data: &serde_json::Value,
|
||||||
|
replies: Vec<Comment>,
|
||||||
|
post_link: &str,
|
||||||
|
post_author: &str,
|
||||||
|
highlighted_comment: &str,
|
||||||
|
filters: &HashSet<String>,
|
||||||
|
req: &Request<Body>,
|
||||||
|
) -> Comment {
|
||||||
|
let id = val(&comment, "id");
|
||||||
|
|
||||||
|
let body = if (val(&comment, "author") == "[deleted]" && val(&comment, "body") == "[removed]") || val(&comment, "body") == "[ Removed by Reddit ]" {
|
||||||
|
format!(
|
||||||
|
"<div class=\"md\"><p>[removed] — <a href=\"https://www.unddit.com{}{}\">view removed comment</a></p></div>",
|
||||||
|
post_link, id
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
rewrite_urls(&val(&comment, "body_html"))
|
||||||
|
};
|
||||||
|
let kind = comment["kind"].as_str().unwrap_or_default().to_string();
|
||||||
|
|
||||||
|
let unix_time = data["created_utc"].as_f64().unwrap_or_default();
|
||||||
|
let (rel_time, created) = time(unix_time);
|
||||||
|
|
||||||
|
let edited = data["edited"].as_f64().map_or((String::new(), String::new()), time);
|
||||||
|
|
||||||
|
let score = data["score"].as_i64().unwrap_or(0);
|
||||||
|
|
||||||
|
// The JSON API only provides comments up to some threshold.
|
||||||
|
// Further comments have to be loaded by subsequent requests.
|
||||||
|
// The "kind" value will be "more" and the "count"
|
||||||
|
// shows how many more (sub-)comments exist in the respective nesting level.
|
||||||
|
// Note that in certain (seemingly random) cases, the count is simply wrong.
|
||||||
|
let more_count = data["count"].as_i64().unwrap_or_default();
|
||||||
|
|
||||||
|
let awards: Awards = Awards::parse(&data["all_awardings"]);
|
||||||
|
|
||||||
|
let parent_kind_and_id = val(&comment, "parent_id");
|
||||||
|
let parent_info = parent_kind_and_id.split('_').collect::<Vec<&str>>();
|
||||||
|
|
||||||
|
let highlighted = id == highlighted_comment;
|
||||||
|
|
||||||
|
let author = Author {
|
||||||
|
name: val(&comment, "author"),
|
||||||
|
flair: Flair {
|
||||||
|
flair_parts: FlairPart::parse(
|
||||||
|
data["author_flair_type"].as_str().unwrap_or_default(),
|
||||||
|
data["author_flair_richtext"].as_array(),
|
||||||
|
data["author_flair_text"].as_str(),
|
||||||
|
),
|
||||||
|
text: val(&comment, "link_flair_text"),
|
||||||
|
background_color: val(&comment, "author_flair_background_color"),
|
||||||
|
foreground_color: val(&comment, "author_flair_text_color"),
|
||||||
|
},
|
||||||
|
distinguished: val(&comment, "distinguished"),
|
||||||
|
};
|
||||||
|
let is_filtered = filters.contains(&["u_", author.name.as_str()].concat());
|
||||||
|
|
||||||
|
// Many subreddits have a default comment posted about the sub's rules etc.
|
||||||
|
// Many libreddit users do not wish to see this kind of comment by default.
|
||||||
|
// Reddit does not tell us which users are "bots", so a good heuristic is to
|
||||||
|
// collapse stickied moderator comments.
|
||||||
|
let is_moderator_comment = data["distinguished"].as_str().unwrap_or_default() == "moderator";
|
||||||
|
let is_stickied = data["stickied"].as_bool().unwrap_or_default();
|
||||||
|
let collapsed = (is_moderator_comment && is_stickied) || is_filtered;
|
||||||
|
|
||||||
|
return Comment {
|
||||||
|
id,
|
||||||
|
kind,
|
||||||
|
parent_id: parent_info[1].to_string(),
|
||||||
|
parent_kind: parent_info[0].to_string(),
|
||||||
|
post_link: post_link.to_string(),
|
||||||
|
post_author: post_author.to_string(),
|
||||||
|
body,
|
||||||
|
author,
|
||||||
|
score: if data["score_hidden"].as_bool().unwrap_or_default() {
|
||||||
|
("\u{2022}".to_string(), "Hidden".to_string())
|
||||||
|
} else {
|
||||||
|
format_num(score)
|
||||||
|
},
|
||||||
|
rel_time,
|
||||||
|
created,
|
||||||
|
edited,
|
||||||
|
replies,
|
||||||
|
highlighted,
|
||||||
|
awards,
|
||||||
|
collapsed,
|
||||||
|
is_filtered,
|
||||||
|
more_count,
|
||||||
|
prefs: Preferences::new(&req),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -629,6 +629,15 @@ button.submit:hover > svg { stroke: var(--accent); }
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#commentQueryForms {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
#allCommentsLink {
|
||||||
|
color: var(--green);
|
||||||
|
}
|
||||||
|
|
||||||
#sort, #search_sort {
|
#sort, #search_sort {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -1550,6 +1559,7 @@ td, th {
|
||||||
#user, #sidebar { margin: 20px 0; }
|
#user, #sidebar { margin: 20px 0; }
|
||||||
#logo, #links { margin-bottom: 5px; }
|
#logo, #links { margin-bottom: 5px; }
|
||||||
#searchbox { width: calc(100vw - 35px); }
|
#searchbox { width: calc(100vw - 35px); }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 480px) {
|
@media screen and (max-width: 480px) {
|
||||||
|
@ -1623,4 +1633,9 @@ td, th {
|
||||||
.popup-inner {
|
.popup-inner {
|
||||||
max-width: 80%;
|
max-width: 80%;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
#commentQueryForms {
|
||||||
|
display: initial;
|
||||||
|
justify-content: initial;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -35,7 +35,7 @@
|
||||||
<div class="comment_body {% if highlighted %}highlighted{% endif %}">{{ body|safe }}</div>
|
<div class="comment_body {% if highlighted %}highlighted{% endif %}">{{ body|safe }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<blockquote class="replies">{% for c in replies -%}{{ c.render().unwrap()|safe }}{%- endfor %}
|
<blockquote class="replies">{% for c in replies -%}{{ c.render().unwrap()|safe }}{%- endfor %}
|
||||||
</blockquote>
|
</bockquote>
|
||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -43,18 +43,32 @@
|
||||||
{% call utils::post(post) %}
|
{% call utils::post(post) %}
|
||||||
|
|
||||||
<!-- SORT FORM -->
|
<!-- SORT FORM -->
|
||||||
|
<div id="commentQueryForms">
|
||||||
<form id="sort">
|
<form id="sort">
|
||||||
<p id="comment_count">{{post.comments.0}} {% if post.comments.0 == "1" %}comment{% else %}comments{% endif %} <span id="sorted_by">sorted by </span></p>
|
<p id="comment_count">{{post.comments.0}} {% if post.comments.0 == "1" %}comment{% else %}comments{% endif %} <span id="sorted_by">sorted by </span></p>
|
||||||
<select name="sort" title="Sort comments by">
|
<select name="sort" title="Sort comments by" id="commentSortSelect">
|
||||||
{% call utils::options(sort, ["confidence", "top", "new", "controversial", "old"], "confidence") %}
|
{% call utils::options(sort, ["confidence", "top", "new", "controversial", "old"], "confidence") %}
|
||||||
</select><button id="sort_submit" class="submit">
|
</select>
|
||||||
<svg width="15" viewBox="0 0 110 100" fill="none" stroke-width="10" stroke-linecap="round">
|
<button id="sort_submit" class="submit">
|
||||||
<path d="M20 50 H100" />
|
<svg width="15" viewBox="0 0 110 100" fill="none" stroke-width="10" stroke-linecap="round">
|
||||||
<path d="M75 15 L100 50 L75 85" />
|
<path d="M20 50 H100" />
|
||||||
→
|
<path d="M75 15 L100 50 L75 85" />
|
||||||
</svg>
|
→
|
||||||
</button>
|
</svg>
|
||||||
</form>
|
</button>
|
||||||
|
</form>
|
||||||
|
<!-- SEARCH FORM -->
|
||||||
|
<form id="sort">
|
||||||
|
<input id="search" type="search" name="q" value="{{ comment_query }}" placeholder="Search comments">
|
||||||
|
<input type="hidden" name="type" value="comment">
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{% if comment_query != "" %}
|
||||||
|
Comments containing "{{ comment_query }}" | <a id="allCommentsLink" href="{{ url_without_query }}">All comments</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- COMMENTS -->
|
<!-- COMMENTS -->
|
||||||
{% for c in comments -%}
|
{% for c in comments -%}
|
||||||
|
|
|
@ -29,7 +29,7 @@
|
||||||
→
|
→
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{% if !is_filtered %}
|
{% if !is_filtered %}
|
||||||
{% if subreddits.len() > 0 || params.typed == "sr_user" %}
|
{% if subreddits.len() > 0 || params.typed == "sr_user" %}
|
||||||
|
|
Loading…
Reference in a new issue