Merge pull request #771 from gmnsii/comment-search

This commit is contained in:
Matthew Esposito 2023-04-15 10:55:10 -04:00 committed by GitHub
commit aaf05de1a8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 185 additions and 101 deletions

View File

@ -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;

View File

@ -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,8 +114,67 @@ 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 replies: Vec<Comment> = if data["replies"].is_object() {
parse_comments(&data["replies"], post_link, post_author, highlighted_comment, filters, req)
} else {
Vec::new()
};
build_comment(&comment, data, replies, post_link, post_author, highlighted_comment, filters, req)
})
.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 unix_time = data["created_utc"].as_f64().unwrap_or_default();
let (rel_time, created) = time(unix_time); let (rel_time, created) = time(unix_time);
@ -114,30 +190,13 @@ fn parse_comments(json: &serde_json::Value, post_link: &str, post_author: &str,
// Note that in certain (seemingly random) cases, the count is simply wrong. // Note that in certain (seemingly random) cases, the count is simply wrong.
let more_count = data["count"].as_i64().unwrap_or_default(); 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() {
parse_comments(&data["replies"], post_link, post_author, highlighted_comment, filters, req)
} else {
Vec::new()
};
let awards: Awards = Awards::parse(&data["all_awardings"]); let awards: Awards = Awards::parse(&data["all_awardings"]);
let parent_kind_and_id = val(&comment, "parent_id"); let parent_kind_and_id = val(&comment, "parent_id");
let parent_info = parent_kind_and_id.split('_').collect::<Vec<&str>>(); let parent_info = parent_kind_and_id.split('_').collect::<Vec<&str>>();
let id = val(&comment, "id");
let highlighted = id == highlighted_comment; 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 { let author = Author {
name: val(&comment, "author"), name: val(&comment, "author"),
flair: Flair { flair: Flair {
@ -162,7 +221,7 @@ fn parse_comments(json: &serde_json::Value, post_link: &str, post_author: &str,
let is_stickied = data["stickied"].as_bool().unwrap_or_default(); let is_stickied = data["stickied"].as_bool().unwrap_or_default();
let collapsed = (is_moderator_comment && is_stickied) || is_filtered; let collapsed = (is_moderator_comment && is_stickied) || is_filtered;
Comment { return Comment {
id, id,
kind, kind,
parent_id: parent_info[1].to_string(), parent_id: parent_info[1].to_string(),
@ -185,8 +244,6 @@ fn parse_comments(json: &serde_json::Value, post_link: &str, post_author: &str,
collapsed, collapsed,
is_filtered, is_filtered,
more_count, more_count,
prefs: Preferences::new(req), prefs: Preferences::new(&req),
} };
})
.collect()
} }

View File

@ -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;
}
} }

View File

@ -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 %}

View File

@ -43,11 +43,13 @@
{% 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>
<button id="sort_submit" class="submit">
<svg width="15" viewBox="0 0 110 100" fill="none" stroke-width="10" stroke-linecap="round"> <svg width="15" viewBox="0 0 110 100" fill="none" stroke-width="10" stroke-linecap="round">
<path d="M20 50 H100" /> <path d="M20 50 H100" />
<path d="M75 15 L100 50 L75 85" /> <path d="M75 15 L100 50 L75 85" />
@ -55,6 +57,18 @@
</svg> </svg>
</button> </button>
</form> </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 }}"&nbsp;|&nbsp;<a id="allCommentsLink" href="{{ url_without_query }}">All comments</a>
{% endif %}
</div>
<!-- COMMENTS --> <!-- COMMENTS -->
{% for c in comments -%} {% for c in comments -%}