M src/main.rs => src/main.rs +1 -1
@@ 90,7 90,7 @@ impl Into<activitystreams::primitives::OneOrMany<activitystreams::base::AnyBase>
pub type ParamSlice<'a> = &'a [&'a (dyn tokio_postgres::types::ToSql + Sync)];
-#[derive(Serialize, Default)]
+#[derive(Serialize, Default, Clone, Copy)]
pub struct Empty {}
pub struct Pineapple {
M => +61 -15
@@ 1,6 1,6 @@
use super::{
JustURL, MaybeIncludeYour, RespAvatarInfo, RespMinimalAuthorInfo, RespMinimalCommentInfo,
RespMinimalPostInfo, RespPostCommentInfo,
JustURL, MaybeIncludeYour, RespAvatarInfo, RespList, RespMinimalAuthorInfo,
RespMinimalCommentInfo, RespMinimalPostInfo, RespPostCommentInfo,
};
use crate::{CommentLocalID, CommunityLocalID, PostLocalID, UserLocalID};
use serde_derive::{Deserialize, Serialize};
@@ 39,7 39,7 @@ async fn route_unstable_comments_get(
let (row, your_vote) = futures::future::try_join(
db.query_opt(
"SELECT reply.author, reply.post, reply.content_text, reply.created, reply.local, reply.content_html, person.username, person.local, person.ap_id, post.title, reply.deleted, reply.parent, person.avatar, reply.attachment_href, (SELECT COUNT(*) FROM reply_like WHERE reply = reply.id) FROM reply INNER JOIN post ON (reply.post = post.id) LEFT OUTER JOIN person ON (reply.author = person.id) WHERE reply.id = $1",
"SELECT reply.author, reply.post, reply.content_text, reply.created, reply.local, reply.content_html, person.username, person.local, person.ap_id, post.title, reply.deleted, reply.parent, person.avatar, reply.attachment_href, (SELECT COUNT(*) FROM reply_like WHERE reply = reply.id), EXISTS(SELECT 1 FROM reply AS r2 WHERE r2.parent = reply.id) FROM reply INNER JOIN post ON (reply.post = post.id) LEFT OUTER JOIN person ON (reply.author = person.id) WHERE reply.id = $1",
&[&comment_id],
)
.map_err(crate::Error::from),
@@ 96,12 96,6 @@ async fn route_unstable_comments_get(
None => None,
};
let replies =
super::get_comments_replies(&[comment_id], include_your_for, 3, &db, &ctx)
.await?
.remove(&comment_id)
.unwrap_or_else(Vec::new);
let output = RespCommentInfo {
base: RespPostCommentInfo {
base: RespMinimalCommentInfo {
@@ 123,8 117,11 @@ async fn route_unstable_comments_get(
created: created.to_rfc3339(),
deleted: row.get(10),
local: row.get(4),
has_replies: !replies.is_empty(),
replies: Some(replies),
replies: if row.get(15) {
None
} else {
Some(RespList::empty())
},
score: row.get(14),
your_vote,
},
@@ 406,10 403,10 @@ async fn route_unstable_comments_likes_list(
})
.collect::<Vec<_>>();
let body = serde_json::json!({
"items": likes,
"next_page": next_page,
});
let body = RespList {
items: (&likes).into(),
next_page: next_page.as_deref().map(Cow::Borrowed),
};
crate::json_response(&body)
}
@@ 516,6 513,54 @@ async fn route_unstable_comments_unlike(
Ok(crate::empty_response())
}
async fn route_unstable_comments_replies_list(
params: (CommentLocalID,),
ctx: Arc<crate::RouteContext>,
req: hyper::Request<hyper::Body>,
) -> Result<hyper::Response<hyper::Body>, crate::Error> {
let (comment_id,) = params;
#[derive(Deserialize)]
struct RepliesListQuery {
#[serde(default)]
include_your: bool,
#[serde(default = "super::default_replies_depth")]
depth: u8,
#[serde(default = "super::default_replies_limit")]
limit: u8,
}
let query: RepliesListQuery = serde_urlencoded::from_str(req.uri().query().unwrap_or(""))?;
let db = ctx.db_pool.get().await?;
let include_your_for = if query.include_your {
let user = crate::require_login(&req, &db).await?;
Some(user)
} else {
None
};
let (replies, next_page) = super::get_comments_replies(
&[comment_id],
include_your_for,
query.depth,
query.limit,
&db,
&ctx,
)
.await?
.remove(&comment_id)
.unwrap_or_else(|| (Vec::new(), None));
let body = RespList {
items: (&replies).into(),
next_page: next_page.as_deref().map(Cow::Borrowed),
};
crate::json_response(&body)
}
async fn route_unstable_comments_replies_create(
params: (CommentLocalID,),
ctx: Arc<crate::RouteContext>,
@@ 595,6 640,7 @@ pub fn route_comments() -> crate::RouteNode<()> {
.with_child(
"replies",
crate::RouteNode::new()
.with_handler_async("GET", route_unstable_comments_replies_list)
.with_handler_async("POST", route_unstable_comments_replies_create),
)
.with_child(
M src/routes/api/mod.rs => src/routes/api/mod.rs +185 -22
@@ 22,6 22,52 @@ lazy_static::lazy_static! {
};
}
+#[derive(Debug)]
+struct InvalidNumber58;
+
+fn parse_number_58(src: &str) -> Result<i64, InvalidNumber58> {
+ let mut buf = [0; 8];
+ match bs58::decode(src).into(&mut buf) {
+ Err(_) => Err(InvalidNumber58),
+ Ok(count) => {
+ if count == 8 {
+ Ok(i64::from_be_bytes(buf))
+ } else {
+ Err(InvalidNumber58)
+ }
+ }
+ }
+}
+
+fn format_number_58(src: i64) -> String {
+ bs58::encode(src.to_be_bytes()).into_string()
+}
+
+struct ValueConsumer<'a> {
+ targets: Vec<&'a mut Option<Box<dyn tokio_postgres::types::ToSql + Send + Sync>>>,
+ start_idx: usize,
+ used: usize,
+}
+
+impl<'a> ValueConsumer<'a> {
+ fn push(&mut self, value: impl tokio_postgres::types::ToSql + Sync + Send + 'static) -> usize {
+ *self.targets[self.used] = Some(Box::new(value));
+ self.used += 1;
+
+ self.start_idx + self.used
+ }
+}
+
+struct InvalidPage;
+impl InvalidPage {
+ fn to_user_error(self) -> crate::Error {
+ crate::Error::UserError(crate::simple_response(
+ hyper::StatusCode::BAD_REQUEST,
+ "Invalid page",
+ ))
+ }
+}
+
#[derive(Deserialize)]
#[serde(rename_all = "snake_case")]
enum SortType {
@@ 43,6 89,76 @@ impl SortType {
SortType::New => "reply.created DESC",
}
}
+
+ pub fn handle_page(
+ &self,
+ page: Option<&str>,
+ mut value_out: ValueConsumer,
+ ) -> Result<(Option<String>, Option<String>), InvalidPage> {
+ match page {
+ None => Ok((None, None)),
+ Some(page) => match self {
+ SortType::Hot => {
+ let page: i64 = parse_number_58(page).map_err(|_| InvalidPage)?;
+ let idx = value_out.push(page);
+ Ok((None, Some(format!(" OFFSET ${}", idx))))
+ }
+ SortType::New => {
+ let page: (chrono::DateTime<chrono::offset::FixedOffset>, i64) = {
+ let mut spl = page.split(',');
+
+ let ts = spl.next().ok_or(InvalidPage)?;
+ let u = spl.next().ok_or(InvalidPage)?;
+ if spl.next().is_some() {
+ return Err(InvalidPage);
+ } else {
+ use chrono::TimeZone;
+
+ let ts: i64 = ts.parse().map_err(|_| InvalidPage)?;
+ let u: i64 = u.parse().map_err(|_| InvalidPage)?;
+
+ let ts = chrono::offset::Utc.timestamp_nanos(ts);
+
+ (ts.into(), u)
+ }
+ };
+
+ let idx1 = value_out.push(page.0);
+ let idx2 = value_out.push(page.1);
+
+ Ok((
+ Some(format!(
+ " AND created < ${0} OR (created = ${0} AND id <= ${1})",
+ idx1, idx2
+ )),
+ None,
+ ))
+ }
+ },
+ }
+ }
+
+ fn get_next_comments_page(
+ &self,
+ comment: RespPostCommentInfo,
+ limit: u8,
+ current_page: Option<&str>,
+ ) -> String {
+ match self {
+ SortType::Hot => format_number_58(
+ i64::from(limit)
+ + match current_page {
+ None => 0,
+ Some(current_page) => parse_number_58(current_page).unwrap(),
+ },
+ ),
+ SortType::New => {
+ let ts: chrono::DateTime<chrono::offset::FixedOffset> =
+ comment.created.parse().unwrap();
+ format!("{},{}", ts, comment.base.id)
+ }
+ }
+ }
}
#[derive(Serialize)]
@@ 56,12 172,27 @@ struct MaybeIncludeYour {
pub include_your: bool,
}
-#[derive(Serialize)]
+#[derive(Serialize, Clone)]
+struct RespList<'a, T: serde::Serialize + ToOwned + Clone> {
+ items: Cow<'a, [T]>,
+ next_page: Option<Cow<'a, str>>,
+}
+
+impl<'a, T: serde::Serialize + ToOwned + Clone> RespList<'a, T> {
+ pub fn empty() -> Self {
+ Self {
+ items: Cow::Borrowed(&[]),
+ next_page: None,
+ }
+ }
+}
+
+#[derive(Serialize, Clone)]
struct RespAvatarInfo<'a> {
url: Cow<'a, str>,
}
-#[derive(Serialize)]
+#[derive(Serialize, Clone)]
struct RespMinimalAuthorInfo<'a> {
id: UserLocalID,
username: Cow<'a, str>,
@@ 80,7 211,7 @@ struct RespLoginUserInfo<'a> {
has_unread_notifications: bool,
}
-#[derive(Serialize)]
+#[derive(Serialize, Clone)]
struct JustUser<'a> {
user: RespMinimalAuthorInfo<'a>,
}
@@ 118,7 249,7 @@ struct RespPostListPost<'a> {
your_vote: Option<Option<crate::Empty>>,
}
-#[derive(Serialize)]
+#[derive(Serialize, Clone)]
struct RespMinimalCommentInfo<'a> {
id: CommentLocalID,
content_text: Option<Cow<'a, str>>,
@@ 126,12 257,12 @@ struct RespMinimalCommentInfo<'a> {
content_html_safe: Option<String>,
}
-#[derive(Serialize)]
+#[derive(Serialize, Clone)]
struct JustURL<'a> {
url: Cow<'a, str>,
}
-#[derive(Serialize)]
+#[derive(Serialize, Clone)]
struct RespPostCommentInfo<'a> {
#[serde(flatten)]
base: RespMinimalCommentInfo<'a>,
@@ 141,13 272,21 @@ struct RespPostCommentInfo<'a> {
created: String,
deleted: bool,
local: bool,
- replies: Option<Vec<RespPostCommentInfo<'a>>>,
- has_replies: bool,
+ replies: Option<RespList<'a, RespPostCommentInfo<'a>>>,
score: i64,
#[serde(skip_serializing_if = "Option::is_none")]
your_vote: Option<Option<crate::Empty>>,
}
+impl<'a> RespPostCommentInfo<'a> {
+ fn has_replies(&self) -> Option<bool> {
+ match &self.replies {
+ None => None,
+ Some(list) => Some(!list.items.is_empty()),
+ }
+ }
+}
+
#[derive(Serialize)]
#[serde(tag = "type")]
enum RespThingInfo<'a> {
@@ 170,6 309,14 @@ enum RespThingInfo<'a> {
},
}
+pub fn default_replies_depth() -> u8 {
+ 3
+}
+
+pub fn default_replies_limit() -> u8 {
+ 30
+}
+
pub fn route_api() -> crate::RouteNode<()> {
crate::RouteNode::new()
.with_child(
@@ 651,6 798,7 @@ async fn apply_comments_replies<'a, T>(
comments: &mut Vec<(T, RespPostCommentInfo<'a>)>,
include_your_for: Option<UserLocalID>,
depth: u8,
+ limit: u8,
db: &tokio_postgres::Client,
ctx: &'a crate::BaseContext,
) -> Result<(), crate::Error> {
@@ 660,12 808,16 @@ async fn apply_comments_replies<'a, T>(
.collect::<Vec<_>>();
if depth > 0 {
let mut replies =
- get_comments_replies_box(&ids, include_your_for, depth - 1, db, ctx).await?;
+ get_comments_replies_box(&ids, include_your_for, depth - 1, limit, db, ctx).await?;
for (_, comment) in comments.iter_mut() {
- let current = replies.remove(&comment.base.id).unwrap_or_else(Vec::new);
- comment.has_replies = !current.is_empty();
- comment.replies = Some(current);
+ let (current, next_page) = replies
+ .remove(&comment.base.id)
+ .unwrap_or_else(|| (Vec::new(), None));
+ comment.replies = Some(RespList {
+ items: current.into(),
+ next_page: next_page.map(From::from),
+ });
}
} else {
use futures::stream::TryStreamExt;
@@ 684,11 836,15 @@ async fn apply_comments_replies<'a, T>(
.await?;
for (_, comment) in comments.iter_mut() {
- comment.has_replies = with_replies.contains(&comment.base.id);
+ comment.replies = if with_replies.contains(&comment.base.id) {
+ None
+ } else {
+ Some(RespList::empty())
+ };
}
}
- comments.retain(|(_, comment)| !comment.deleted || comment.has_replies);
+ comments.retain(|(_, comment)| !comment.deleted || comment.has_replies() != Some(false));
Ok(())
}
@@ 697,13 853,14 @@ fn get_comments_replies_box<'a: 'b, 'b>(
parents: &'b [CommentLocalID],
include_your_for: Option<UserLocalID>,
depth: u8,
+ limit: u8,
db: &'b tokio_postgres::Client,
ctx: &'a crate::BaseContext,
) -> std::pin::Pin<
Box<
dyn Future<
Output = Result<
- HashMap<CommentLocalID, Vec<RespPostCommentInfo<'a>>>,
+ HashMap<CommentLocalID, (Vec<RespPostCommentInfo<'a>>, Option<String>)>,
crate::Error,
>,
> + Send
@@ 714,6 871,7 @@ fn get_comments_replies_box<'a: 'b, 'b>(
parents,
include_your_for,
depth,
+ limit,
db,
ctx,
))
@@ 723,12 881,13 @@ async fn get_comments_replies<'a>(
parents: &[CommentLocalID],
include_your_for: Option<UserLocalID>,
depth: u8,
+ limit: u8,
db: &tokio_postgres::Client,
ctx: &'a crate::BaseContext,
-) -> Result<HashMap<CommentLocalID, Vec<RespPostCommentInfo<'a>>>, crate::Error> {
+) -> Result<HashMap<CommentLocalID, (Vec<RespPostCommentInfo<'a>>, Option<String>)>, crate::Error> {
use futures::TryStreamExt;
- let sql1 = "SELECT reply.id, reply.author, reply.content_text, reply.created, reply.parent, reply.content_html, person.username, person.local, person.ap_id, reply.deleted, person.avatar, reply.attachment_href, reply.local, (SELECT COUNT(*) FROM reply_like WHERE reply = reply.id)";
+ let sql1 = "SELECT result.* FROM UNNEST($1::BIGINT[]) JOIN LATERAL (SELECT reply.id, reply.author, reply.content_text, reply.created, reply.parent, reply.content_html, person.username, person.local, person.ap_id, reply.deleted, person.avatar, reply.attachment_href, reply.local, (SELECT COUNT(*) FROM reply_like WHERE reply = reply.id)";
let (sql2, values): (_, Vec<&(dyn tokio_postgres::types::ToSql + Sync)>) =
if include_your_for.is_some() {
(
@@ 738,7 897,7 @@ async fn get_comments_replies<'a>(
} else {
("", vec![&parents])
};
- let sql3 = " FROM reply LEFT OUTER JOIN person ON (person.id = reply.author) WHERE parent = ANY($1::BIGINT[]) ORDER BY hot_rank((SELECT COUNT(*) FROM reply_like WHERE reply = reply.id AND person != reply.author), reply.created) DESC";
+ let sql3 = " FROM reply LEFT OUTER JOIN person ON (person.id = reply.author) WHERE parent = unnest ORDER BY hot_rank((SELECT COUNT(*) FROM reply_like WHERE reply = reply.id AND person != reply.author), reply.created) DESC) AS result ON TRUE";
let sql: &str = &format!("{}{}{}", sql1, sql2, sql3);
@@ 795,8 954,7 @@ async fn get_comments_replies<'a>(
created: created.to_rfc3339(),
deleted: row.get(9),
local: row.get(12),
- replies: None,
- has_replies: false,
+ replies: Some(RespList::empty()),
score: row.get(13),
your_vote: match include_your_for {
None => None,
@@ 812,11 970,16 @@ async fn get_comments_replies<'a>(
.try_collect()
.await?;
- apply_comments_replies(&mut comments, include_your_for, depth, db, &ctx).await?;
+ apply_comments_replies(&mut comments, include_your_for, depth, limit, db, &ctx).await?;
let mut result = HashMap::new();
for (parent, comment) in comments {
- result.entry(parent).or_insert_with(Vec::new).push(comment);
+ let entry = result.entry(parent).or_insert_with(|| (Vec::new(), None));
+ if entry.0.len() < limit.into() {
+ entry.0.push(comment);
+ } else {
+ entry.1 = Some(limit.to_string());
+ }
}
Ok(result)
M src/routes/api/posts.rs => src/routes/api/posts.rs +100 -23
@@ 1,6 1,6 @@
use super::{
- JustURL, RespAvatarInfo, RespMinimalAuthorInfo, RespMinimalCommentInfo,
- RespMinimalCommunityInfo, RespPostCommentInfo, RespPostListPost,
+ JustURL, RespAvatarInfo, RespList, RespMinimalAuthorInfo, RespMinimalCommentInfo,
+ RespMinimalCommunityInfo, RespPostCommentInfo, RespPostListPost, ValueConsumer,
};
use crate::{CommentLocalID, CommunityLocalID, PostLocalID, UserLocalID};
use serde_derive::{Deserialize, Serialize};
@@ 13,24 13,55 @@ async fn get_post_comments<'a>(
post_id: PostLocalID,
include_your_for: Option<UserLocalID>,
sort: super::SortType,
+ limit: u8,
+ page: Option<&'a str>,
db: &tokio_postgres::Client,
ctx: &'a crate::BaseContext,
-) -> Result<Vec<RespPostCommentInfo<'a>>, crate::Error> {
+) -> Result<(Vec<RespPostCommentInfo<'a>>, Option<String>), crate::Error> {
use futures::TryStreamExt;
+ let limit_i = i64::from(limit) + 1;
+
let sql1 = "SELECT reply.id, reply.author, reply.content_text, reply.created, reply.content_html, person.username, person.local, person.ap_id, reply.deleted, person.avatar, attachment_href, reply.local, (SELECT COUNT(*) FROM reply_like WHERE reply = reply.id)";
- let (sql2, values): (_, Vec<&(dyn tokio_postgres::types::ToSql + Sync)>) =
+ let (sql2, mut values): (_, Vec<&(dyn tokio_postgres::types::ToSql + Sync)>) =
if include_your_for.is_some() {
(
- ", EXISTS(SELECT 1 FROM reply_like WHERE reply = reply.id AND person = $2)",
- vec![&post_id, &include_your_for],
+ ", EXISTS(SELECT 1 FROM reply_like WHERE reply = reply.id AND person = $3)",
+ vec![&post_id, &limit_i, &include_your_for],
)
} else {
- ("", vec![&post_id])
+ ("", vec![&post_id, &limit_i])
};
- let sql3 = format!(" FROM reply LEFT OUTER JOIN person ON (person.id = reply.author) WHERE post=$1 AND parent IS NULL ORDER BY {}", sort.comment_sort_sql());
+ let mut sql3 = "FROM reply LEFT OUTER JOIN person ON (person.id = reply.author) WHERE post=$1 AND parent IS NULL ".to_owned();
+ let mut sql4 = format!("ORDER BY {} LIMIT $2", sort.comment_sort_sql());
+
+ let mut con1 = None;
+ let mut con2 = None;
+ let (page_part1, page_part2) = sort
+ .handle_page(
+ page,
+ ValueConsumer {
+ targets: vec![&mut con1, &mut con2],
+ start_idx: values.len(),
+ used: 0,
+ },
+ )
+ .map_err(super::InvalidPage::to_user_error)?;
+ if let Some(value) = &con1 {
+ values.push(value.as_ref());
+ if let Some(value) = &con2 {
+ values.push(value.as_ref());
+ }
+ }
+
+ if let Some(part) = page_part1 {
+ sql3.push_str(&part);
+ }
+ if let Some(part) = page_part2 {
+ sql4.push_str(&part);
+ }
- let sql: &str = &format!("{}{}{}", sql1, sql2, sql3);
+ let sql: &str = &format!("{}{}{}{}", sql1, sql2, sql3, sql4);
let stream = crate::query_stream(db, sql, &values[..]).await?;
@@ 84,8 115,7 @@ async fn get_post_comments<'a>(
created: created.to_rfc3339(),
deleted: row.get(8),
local: row.get(11),
- replies: None,
- has_replies: false,
+ replies: Some(RespList::empty()),
score: row.get(12),
your_vote: match include_your_for {
None => None,
@@ 101,9 131,18 @@ async fn get_post_comments<'a>(
.try_collect()
.await?;
- super::apply_comments_replies(&mut comments, include_your_for, 2, db, &ctx).await?;
+ let next_page = if comments.len() > usize::from(limit) {
+ Some(sort.get_next_comments_page(comments.pop().unwrap().1, limit, page))
+ } else {
+ None
+ };
+
+ super::apply_comments_replies(&mut comments, include_your_for, 2, limit, db, &ctx).await?;
- Ok(comments.into_iter().map(|(_, comment)| comment).collect())
+ Ok((
+ comments.into_iter().map(|(_, comment)| comment).collect(),
+ next_page,
+ ))
}
async fn route_unstable_posts_list(
@@ 216,6 255,52 @@ async fn route_unstable_posts_list(
crate::json_response(&posts)
}
+async fn route_unstable_posts_replies_list(
+ params: (PostLocalID,),
+ ctx: Arc<crate::RouteContext>,
+ req: hyper::Request<hyper::Body>,
+) -> Result<hyper::Response<hyper::Body>, crate::Error> {
+ let (post_id,) = params;
+
+ #[derive(Deserialize)]
+ struct RepliesListQuery<'a> {
+ #[serde(default)]
+ include_your: bool,
+ #[serde(default = "super::default_replies_limit")]
+ limit: u8,
+ page: Option<Cow<'a, str>>,
+ }
+
+ let query: RepliesListQuery = serde_urlencoded::from_str(req.uri().query().unwrap_or(""))?;
+
+ let db = ctx.db_pool.get().await?;
+
+ let include_your_for = if query.include_your {
+ let user = crate::require_login(&req, &db).await?;
+ Some(user)
+ } else {
+ None
+ };
+
+ let (replies, next_page) = get_post_comments(
+ post_id,
+ include_your_for,
+ super::SortType::Hot,
+ query.limit,
+ query.page.as_deref(),
+ &db,
+ &ctx,
+ )
+ .await?;
+
+ let body = RespList {
+ items: (&replies).into(),
+ next_page: next_page.as_deref().map(Cow::Borrowed),
+ };
+
+ crate::json_response(&body)
+}
+
async fn route_unstable_posts_create(
_: (),
ctx: Arc<crate::RouteContext>,
@@ 337,16 422,10 @@ async fn route_unstable_posts_get(
) -> Result<hyper::Response<hyper::Body>, crate::Error> {
use futures::future::TryFutureExt;
- fn default_sort() -> super::SortType {
- super::SortType::Hot
- }
-
#[derive(Deserialize)]
struct PostsGetQuery {
#[serde(default)]
include_your: bool,
- #[serde(default = "default_sort")]
- replies_sort: super::SortType,
}
let query: PostsGetQuery = serde_urlencoded::from_str(req.uri().query().unwrap_or(""))?;
@@ 367,18 446,16 @@ async fn route_unstable_posts_get(
post: &'a RespPostListPost<'a>,
approved: bool,
local: bool,
- replies: Vec<RespPostCommentInfo<'a>>,
}
let (post_id,) = params;
- let (row, comments, your_vote) = futures::future::try_join3(
+ let (row, your_vote) = futures::future::try_join(
db.query_opt(
"SELECT post.author, post.href, post.content_text, post.title, post.created, post.content_html, community.id, community.name, community.local, community.ap_id, person.username, person.local, person.ap_id, (SELECT COUNT(*) FROM post_like WHERE post_like.post = $1), post.approved, person.avatar, post.local FROM community, post LEFT OUTER JOIN person ON (person.id = post.author) WHERE post.community = community.id AND post.id = $1",
&[&post_id],
)
.map_err(crate::Error::from),
- get_post_comments(post_id, include_your_for, query.replies_sort, &db, &ctx),
async {
if let Some(user) = include_your_for {
let row = db.query_opt("SELECT 1 FROM post_like WHERE post=$1 AND person=$2", &[&post_id, &user]).await?;
@@ 462,7 539,6 @@ async fn route_unstable_posts_get(
let output = RespPostInfo {
post: &post,
local: row.get(16),
- replies: comments,
approved: row.get(14),
};
@@ 914,6 990,7 @@ pub fn route_posts() -> crate::RouteNode<()> {
.with_child(
"replies",
crate::RouteNode::new()
+ .with_handler_async("GET", route_unstable_posts_replies_list)
.with_handler_async("POST", route_unstable_posts_replies_create),
)
.with_child(