diff --git a/build.rs b/build.rs index 3ee44a4..26b1ed9 100644 --- a/build.rs +++ b/build.rs @@ -1,6 +1,4 @@ -use std::{ - process::{Command, ExitStatus, Output}, -}; +use std::process::{Command, ExitStatus, Output}; #[cfg(not(target_os = "windows"))] use std::os::unix::process::ExitStatusExt; diff --git a/src/post.rs b/src/post.rs index f2f5eaf..91af91e 100644 --- a/src/post.rs +++ b/src/post.rs @@ -8,6 +8,8 @@ use crate::utils::{ use hyper::{Body, Request, Response}; use askama::Template; +use once_cell::sync::Lazy; +use regex::Regex; use std::collections::HashSet; // STRUCTS @@ -20,13 +22,18 @@ struct PostTemplate { prefs: Preferences, single_thread: bool, url: String, + url_without_query: String, + comment_query: String, } +static COMMENT_SEARCH_CAPTURE: Lazy = Lazy::new(|| Regex::new(r#"\?q=(.*)&type=comment"#).unwrap()); + pub async fn item(req: Request) -> Result, String> { // Build Reddit API path 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 quarantined = can_access_quarantine(&req, &sub); + let url = req.uri().to_string(); // Set sort to sort query parameter let sort = param(&path, "sort").unwrap_or_else(|| { @@ -63,17 +70,26 @@ pub async fn item(req: Request) -> Result, String> { 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 url = req.uri().to_string(); + let query = match COMMENT_SEARCH_CAPTURE.captures(&url) { + 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 template(PostTemplate { comments, post, + url_without_query: url.clone().trim_end_matches(&format!("?q={query}&type=comment")).to_string(), sort, prefs: Preferences::new(&req), single_thread, url, + comment_query: query, }) } // If the Reddit API returns an error, exit and send error page to user @@ -89,6 +105,7 @@ pub async fn item(req: Request) -> Result, String> { } // COMMENTS + fn parse_comments(json: &serde_json::Value, post_link: &str, post_author: &str, highlighted_comment: &str, filters: &HashSet, req: &Request) -> Vec { // 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); @@ -97,96 +114,136 @@ fn parse_comments(json: &serde_json::Value, post_link: &str, post_author: &str, comments .into_iter() .map(|comment| { - let kind = comment["kind"].as_str().unwrap_or_default().to_string(); 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 = 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 parent_kind_and_id = val(&comment, "parent_id"); - let parent_info = parent_kind_and_id.split('_').collect::>(); - - 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!( - "

[removed] — view removed comment

", - 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), - } + 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, + query: &str, + req: &Request, +) -> Vec { + 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, + post_link: &str, + post_author: &str, + highlighted_comment: &str, + filters: &HashSet, + req: &Request, +) -> Comment { + let id = val(&comment, "id"); + + let body = if (val(&comment, "author") == "[deleted]" && val(&comment, "body") == "[removed]") || val(&comment, "body") == "[ Removed by Reddit ]" { + format!( + "

[removed] — view removed comment

", + 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::>(); + + 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), + }; +} diff --git a/static/style.css b/static/style.css index 95039f6..9683cff 100644 --- a/static/style.css +++ b/static/style.css @@ -629,6 +629,15 @@ button.submit:hover > svg { stroke: var(--accent); } background: transparent; } +#commentQueryForms { + display: flex; + justify-content: space-between; +} + +#allCommentsLink { + color: var(--green); +} + #sort, #search_sort { display: flex; align-items: center; @@ -1550,6 +1559,7 @@ td, th { #user, #sidebar { margin: 20px 0; } #logo, #links { margin-bottom: 5px; } #searchbox { width: calc(100vw - 35px); } + } @media screen and (max-width: 480px) { @@ -1623,4 +1633,9 @@ td, th { .popup-inner { max-width: 80%; } -} \ No newline at end of file + + #commentQueryForms { + display: initial; + justify-content: initial; + } +} diff --git a/templates/comment.html b/templates/comment.html index f3d0f27..e75b888 100644 --- a/templates/comment.html +++ b/templates/comment.html @@ -35,7 +35,7 @@
{{ body|safe }}
{% endif %}
{% for c in replies -%}{{ c.render().unwrap()|safe }}{%- endfor %} -
+ {% endif %} diff --git a/templates/post.html b/templates/post.html index c79a18d..a996bb4 100644 --- a/templates/post.html +++ b/templates/post.html @@ -43,18 +43,32 @@ {% call utils::post(post) %} +

{{post.comments.0}} {% if post.comments.0 == "1" %}comment{% else %}comments{% endif %} sorted by

- {% call utils::options(sort, ["confidence", "top", "new", "controversial", "old"], "confidence") %} - -
+ + + + +
+ + +
+
+ +
+ {% if comment_query != "" %} + Comments containing "{{ comment_query }}" | All comments + {% endif %} +
{% for c in comments -%} diff --git a/templates/search.html b/templates/search.html index 4fc0c4d..53528e7 100644 --- a/templates/search.html +++ b/templates/search.html @@ -29,7 +29,7 @@ → - + {% if !is_filtered %} {% if subreddits.len() > 0 || params.typed == "sr_user" %}