1#![allow(clippy::cmp_owned)]
2
3use crate::client::json;
5use crate::config::get_setting;
6use crate::server::RequestExt;
7use crate::subreddit::{can_access_quarantine, quarantine};
8use crate::utils::{
9 error, format_num, get_filters, nsfw_landing, param, parse_post, rewrite_emotes, setting, template, time, val, Author, Awards, Comment, Flair, FlairPart, Post, Preferences,
10};
11use hyper::{Body, Request, Response};
12
13use once_cell::sync::Lazy;
14use regex::Regex;
15use rinja::Template;
16use std::collections::{HashMap, HashSet};
17
18#[derive(Template)]
20#[template(path = "post.html")]
21struct PostTemplate {
22 comments: Vec<Comment>,
23 post: Post,
24 sort: String,
25 prefs: Preferences,
26 single_thread: bool,
27 url: String,
28 url_without_query: String,
29 comment_query: String,
30}
31
32static COMMENT_SEARCH_CAPTURE: Lazy<Regex> = Lazy::new(|| Regex::new(r"\?q=(.*)&type=comment").unwrap());
33
34pub async fn item(req: Request<Body>) -> Result<Response<Body>, String> {
35 let mut path: String = format!("{}.json?{}&raw_json=1", req.uri().path(), req.uri().query().unwrap_or_default());
37 let sub = req.param("sub").unwrap_or_default();
38 let quarantined = can_access_quarantine(&req, &sub);
39 let url = req.uri().to_string();
40
41 let sort = param(&path, "sort").unwrap_or_else(|| {
43 let default_sort = setting(&req, "comment_sort");
45
46 if default_sort.is_empty() {
48 String::new()
49 } else {
50 path = format!("{}.json?{}&sort={}&raw_json=1", req.uri().path(), req.uri().query().unwrap_or_default(), default_sort);
51 default_sort
52 }
53 });
54
55 #[cfg(debug_assertions)]
57 req.param("id").unwrap_or_default();
58
59 let single_thread = req.param("comment_id").is_some();
60 let highlighted_comment = &req.param("comment_id").unwrap_or_default();
61
62 match json(path, quarantined).await {
64 Ok(response) => {
66 let post = parse_post(&response[0]["data"]["children"][0]).await;
68
69 let req_url = req.uri().to_string();
70 if post.nsfw && crate::utils::should_be_nsfw_gated(&req, &req_url) {
74 return Ok(nsfw_landing(req, req_url).await.unwrap_or_default());
75 }
76
77 let query_body = match COMMENT_SEARCH_CAPTURE.captures(&url) {
78 Some(captures) => captures.get(1).unwrap().as_str().replace("%20", " ").replace('+', " "),
79 None => String::new(),
80 };
81
82 let query_string = format!("q={query_body}&type=comment");
83 let form = url::form_urlencoded::parse(query_string.as_bytes()).collect::<HashMap<_, _>>();
84 let query = form.get("q").unwrap().clone().to_string();
85
86 let comments = match query.as_str() {
87 "" => parse_comments(&response[1], &post.permalink, &post.author.name, highlighted_comment, &get_filters(&req), &req),
88 _ => query_comments(&response[1], &post.permalink, &post.author.name, highlighted_comment, &get_filters(&req), &query, &req),
89 };
90
91 Ok(template(&PostTemplate {
93 comments,
94 post,
95 url_without_query: url.clone().trim_end_matches(&format!("?q={query}&type=comment")).to_string(),
96 sort,
97 prefs: Preferences::new(&req),
98 single_thread,
99 url: req_url,
100 comment_query: query,
101 }))
102 }
103 Err(msg) => {
105 if msg == "quarantined" || msg == "gated" {
106 let sub = req.param("sub").unwrap_or_default();
107 Ok(quarantine(&req, sub, &msg))
108 } else {
109 error(req, &msg).await
110 }
111 }
112 }
113}
114
115fn parse_comments(json: &serde_json::Value, post_link: &str, post_author: &str, highlighted_comment: &str, filters: &HashSet<String>, req: &Request<Body>) -> Vec<Comment> {
118 let comments = json["data"]["children"].as_array().map_or(Vec::new(), std::borrow::ToOwned::to_owned);
120
121 comments
123 .into_iter()
124 .map(|comment| {
125 let data = &comment["data"];
126 let replies: Vec<Comment> = if data["replies"].is_object() {
127 parse_comments(&data["replies"], post_link, post_author, highlighted_comment, filters, req)
128 } else {
129 Vec::new()
130 };
131 build_comment(&comment, data, replies, post_link, post_author, highlighted_comment, filters, req)
132 })
133 .collect()
134}
135
136fn query_comments(
137 json: &serde_json::Value,
138 post_link: &str,
139 post_author: &str,
140 highlighted_comment: &str,
141 filters: &HashSet<String>,
142 query: &str,
143 req: &Request<Body>,
144) -> Vec<Comment> {
145 let comments = json["data"]["children"].as_array().map_or(Vec::new(), std::borrow::ToOwned::to_owned);
146 let mut results = Vec::new();
147
148 for comment in comments {
149 let data = &comment["data"];
150
151 if data["replies"].is_object() {
153 results.append(&mut query_comments(&data["replies"], post_link, post_author, highlighted_comment, filters, query, req));
154 }
155
156 let c = build_comment(&comment, data, Vec::new(), post_link, post_author, highlighted_comment, filters, req);
157 if c.body.to_lowercase().contains(&query.to_lowercase()) {
158 results.push(c);
159 }
160 }
161
162 results
163}
164#[allow(clippy::too_many_arguments)]
165fn build_comment(
166 comment: &serde_json::Value,
167 data: &serde_json::Value,
168 replies: Vec<Comment>,
169 post_link: &str,
170 post_author: &str,
171 highlighted_comment: &str,
172 filters: &HashSet<String>,
173 req: &Request<Body>,
174) -> Comment {
175 let id = val(comment, "id");
176
177 let body = if (val(comment, "author") == "[deleted]" && val(comment, "body") == "[removed]") || val(comment, "body") == "[ Removed by Reddit ]" {
178 format!(
179 "<div class=\"md\"><p>[removed] — <a href=\"https://{}{post_link}{id}\">view removed comment</a></p></div>",
180 get_setting("REDLIB_PUSHSHIFT_FRONTEND").unwrap_or_else(|| String::from(crate::config::DEFAULT_PUSHSHIFT_FRONTEND)),
181 )
182 } else {
183 rewrite_emotes(&data["media_metadata"], val(comment, "body_html"))
184 };
185 let kind = comment["kind"].as_str().unwrap_or_default().to_string();
186
187 let unix_time = data["created_utc"].as_f64().unwrap_or_default();
188 let (rel_time, created) = time(unix_time);
189
190 let edited = data["edited"].as_f64().map_or((String::new(), String::new()), time);
191
192 let score = data["score"].as_i64().unwrap_or(0);
193
194 let more_count = data["count"].as_i64().unwrap_or_default();
200
201 let awards: Awards = Awards::parse(&data["all_awardings"]);
202
203 let parent_kind_and_id = val(comment, "parent_id");
204 let parent_info = parent_kind_and_id.split('_').collect::<Vec<&str>>();
205
206 let highlighted = id == highlighted_comment;
207
208 let author = Author {
209 name: val(comment, "author"),
210 flair: Flair {
211 flair_parts: FlairPart::parse(
212 data["author_flair_type"].as_str().unwrap_or_default(),
213 data["author_flair_richtext"].as_array(),
214 data["author_flair_text"].as_str(),
215 ),
216 text: val(comment, "link_flair_text"),
217 background_color: val(comment, "author_flair_background_color"),
218 foreground_color: val(comment, "author_flair_text_color"),
219 },
220 distinguished: val(comment, "distinguished"),
221 };
222 let is_filtered = filters.contains(&["u_", author.name.as_str()].concat());
223
224 let is_moderator_comment = data["distinguished"].as_str().unwrap_or_default() == "moderator";
229 let is_stickied = data["stickied"].as_bool().unwrap_or_default();
230 let collapsed = (is_moderator_comment && is_stickied) || is_filtered;
231
232 Comment {
233 id,
234 kind,
235 parent_id: parent_info[1].to_string(),
236 parent_kind: parent_info[0].to_string(),
237 post_link: post_link.to_string(),
238 post_author: post_author.to_string(),
239 body,
240 author,
241 score: if data["score_hidden"].as_bool().unwrap_or_default() {
242 ("\u{2022}".to_string(), "Hidden".to_string())
243 } else {
244 format_num(score)
245 },
246 rel_time,
247 created,
248 edited,
249 replies,
250 highlighted,
251 awards,
252 collapsed,
253 is_filtered,
254 more_count,
255 prefs: Preferences::new(req),
256 }
257}