use crate::components::{CommunityLink, HTPage, MaybeFillInput, MaybeFillTextArea, PostItem}; use crate::resp_types::{ JustContentHTML, JustStringID, RespCommunityInfoMaybeYour, RespMinimalAuthorInfo, RespMinimalCommunityInfo, RespPostListPost, RespYourFollow, }; use crate::routes::{ fetch_base_data, for_client, get_cookie_map_for_headers, get_cookie_map_for_req, html_response, res_to_error, CookieMap, }; use serde_derive::Deserialize; use std::borrow::Cow; use std::collections::HashMap; use std::sync::Arc; async fn page_communities( _: (), ctx: Arc, req: hyper::Request, ) -> Result, crate::Error> { let lang = crate::get_lang_for_req(&req); let cookies = get_cookie_map_for_req(&req)?; let base_data = fetch_base_data(&ctx.backend_host, &ctx.http_client, req.headers(), &cookies).await?; let api_res = res_to_error( ctx.http_client .request( hyper::Request::get(format!("{}/api/unstable/communities", ctx.backend_host,)) .body(Default::default())?, ) .await?, ) .await?; let api_res = hyper::body::to_bytes(api_res.into_body()).await?; let communities: Vec = serde_json::from_slice(&api_res)?; let title = lang.tr("communities", None); Ok(html_response(render::html! {

{title.as_ref()}

{lang.tr("local", None)}

{ if base_data.login.is_some() { Some(render::rsx! { {lang.tr("community_create", None)} }) } else { None } }
    { communities.iter() .filter(|x| x.local) .map(|community| { render::rsx! {
  • } }) .collect::>() }

{lang.tr("remote", None)}

{" "}
    { communities.iter() .filter(|x| !x.local) .map(|community| { render::rsx! {
  • } }) .collect::>() }
})) } async fn page_community( params: (i64,), ctx: Arc, req: hyper::Request, ) -> Result, crate::Error> { let (community_id,) = params; fn default_sort() -> crate::SortType { crate::SortType::Hot } #[derive(Deserialize)] struct Query { #[serde(default = "default_sort")] sort: crate::SortType, } let query: Query = serde_urlencoded::from_str(req.uri().query().unwrap_or(""))?; let lang = crate::get_lang_for_req(&req); let cookies = get_cookie_map_for_req(&req)?; // TODO parallelize requests let base_data = fetch_base_data(&ctx.backend_host, &ctx.http_client, req.headers(), &cookies).await?; let community_info_api_res = res_to_error( ctx.http_client .request(for_client( hyper::Request::get(format!( "{}/api/unstable/communities/{}{}", ctx.backend_host, community_id, if base_data.login.is_some() { "?include_your=true" } else { "" }, )) .body(Default::default())?, req.headers(), &cookies, )?) .await?, ) .await?; let community_info_api_res = hyper::body::to_bytes(community_info_api_res.into_body()).await?; let community_info: RespCommunityInfoMaybeYour = { serde_json::from_slice(&community_info_api_res)? }; let posts_api_res = res_to_error( ctx.http_client .request(for_client( hyper::Request::get(format!( "{}/api/unstable/communities/{}/posts?sort={}", ctx.backend_host, community_id, query.sort.as_str(), )) .body(Default::default())?, req.headers(), &cookies, )?) .await?, ) .await?; let posts_api_res = hyper::body::to_bytes(posts_api_res.into_body()).await?; let posts: Vec> = serde_json::from_slice(&posts_api_res)?; let new_post_url = format!("/communities/{}/new_post", community_id); let title = community_info.as_ref().name.as_ref(); Ok(html_response(render::html! {

{title}

{format!("@{}@{}", community_info.as_ref().name, community_info.as_ref().host)}
{ if community_info.as_ref().local { None } else if let Some(remote_url) = &community_info.as_ref().remote_url { Some(render::rsx! {
{lang.tr("community_remote_note", None)} {" "} {lang.tr("view_at_source", None)}{" ↗"}
}) } else { None // shouldn't ever happen } }

{ if base_data.login.is_some() { Some(match community_info.your_follow { Some(RespYourFollow { accepted: true }) => { render::rsx! {

} }, Some(RespYourFollow { accepted: false }) => { render::rsx! {
} }, None => { render::rsx! {
} } }) } else { None } }

{lang.tr("post_new", None)}

{ if community_info.you_are_moderator == Some(true) { Some(render::rsx! {

{lang.tr("community_edit_link", None)}

}) } else { None } }

{community_info.description.as_ref()}

{lang.tr("moderators", None)}

{lang.tr("sort", None)} { crate::SortType::VALUES.iter() .map(|value| { let name = lang.tr(value.lang_key(), None); if query.sort == *value { render::rsx! { {name} } } else { render::rsx! { {name} } } }) .collect::>() }
{ if posts.is_empty() { Some(render::rsx! {

{lang.tr("nothing", None)}

}) } else { None } }
    {posts.iter().map(|post| { PostItem { post, in_community: true, no_user: false, lang: &lang } }).collect::>()}
})) } async fn page_community_edit( params: (i64,), ctx: Arc, req: hyper::Request, ) -> Result, crate::Error> { let (community_id,) = params; let cookies = get_cookie_map_for_req(&req)?; page_community_edit_inner(community_id, req.headers(), &cookies, ctx, None, None).await } async fn page_community_edit_inner( community_id: i64, headers: &hyper::header::HeaderMap, cookies: &CookieMap<'_>, ctx: Arc, display_error: Option, prev_values: Option<&HashMap<&str, serde_json::Value>>, ) -> Result, crate::Error> { let base_data = fetch_base_data(&ctx.backend_host, &ctx.http_client, headers, &cookies).await?; let lang = crate::get_lang_for_headers(headers); let community_info_api_res = res_to_error( ctx.http_client .request(for_client( hyper::Request::get(format!( "{}/api/unstable/communities/{}", ctx.backend_host, community_id, )) .body(Default::default())?, headers, &cookies, )?) .await?, ) .await?; let community_info_api_res = hyper::body::to_bytes(community_info_api_res.into_body()).await?; let community_info: RespCommunityInfoMaybeYour = { serde_json::from_slice(&community_info_api_res)? }; let title = lang.tr("community_edit", None); Ok(html_response(render::html! {

{title.as_ref()}

{community_info.as_ref().name.as_ref()}

{ display_error.map(|msg| { render::rsx! {
{msg}
} }) }
})) } async fn handler_communities_edit_submit( params: (i64,), ctx: Arc, req: hyper::Request, ) -> Result, crate::Error> { let (community_id,) = params; let (req_parts, body) = req.into_parts(); let cookies = get_cookie_map_for_headers(&req_parts.headers)?; let body = hyper::body::to_bytes(body).await?; let body: HashMap<&str, serde_json::Value> = serde_urlencoded::from_bytes(&body)?; let api_res = res_to_error( ctx.http_client .request(for_client( hyper::Request::patch(format!( "{}/api/unstable/communities/{}", ctx.backend_host, community_id )) .body(serde_json::to_vec(&body)?.into())?, &req_parts.headers, &cookies, )?) .await?, ) .await; match api_res { Err(crate::Error::RemoteError((_, message))) => { page_community_edit_inner( community_id, &req_parts.headers, &cookies, ctx, Some(message), Some(&body), ) .await } Err(other) => Err(other), Ok(_) => Ok(hyper::Response::builder() .status(hyper::StatusCode::SEE_OTHER) .header( hyper::header::LOCATION, format!("/communities/{}", community_id), ) .body("Successfully edited.".into())?), } } async fn handler_community_follow( params: (i64,), ctx: Arc, req: hyper::Request, ) -> Result, crate::Error> { let (community_id,) = params; let cookies = get_cookie_map_for_req(&req)?; res_to_error( ctx.http_client .request(for_client( hyper::Request::post(format!( "{}/api/unstable/communities/{}/follow", ctx.backend_host, community_id )) .header(hyper::header::CONTENT_TYPE, "application/json") .body("{\"try_wait_for_accept\":true}".into())?, req.headers(), &cookies, )?) .await?, ) .await?; Ok(hyper::Response::builder() .status(hyper::StatusCode::SEE_OTHER) .header( hyper::header::LOCATION, format!("/communities/{}", community_id), ) .body("Successfully followed".into())?) } async fn page_community_moderators( params: (i64,), ctx: Arc, req: hyper::Request, ) -> Result, crate::Error> { let (community_id,) = params; let lang = crate::get_lang_for_req(&req); let cookies = get_cookie_map_for_req(&req)?; let base_data = fetch_base_data(&ctx.backend_host, &ctx.http_client, req.headers(), &cookies).await?; let community_info_api_res = res_to_error( ctx.http_client .request(for_client( hyper::Request::get(format!( "{}/api/unstable/communities/{}{}", ctx.backend_host, community_id, if base_data.login.is_some() { "?include_your=true" } else { "" }, )) .body(Default::default())?, req.headers(), &cookies, )?) .await?, ) .await?; let community_info_api_res = hyper::body::to_bytes(community_info_api_res.into_body()).await?; let community_info: RespCommunityInfoMaybeYour = { serde_json::from_slice(&community_info_api_res)? }; let api_res = res_to_error( ctx.http_client .request(for_client( hyper::Request::get(format!( "{}/api/unstable/communities/{}/moderators", ctx.backend_host, community_id, )) .body(Default::default())?, req.headers(), &cookies, )?) .await?, ) .await?; let api_res = hyper::body::to_bytes(api_res.into_body()).await?; let api_res: Vec = serde_json::from_slice(&api_res)?; let title = lang.tr("moderators", None); Ok(html_response(render::html! {

{title.as_ref()}

    { api_res.iter().map(|user| { render::rsx! {
  • {user.username.as_ref()} { if community_info.you_are_moderator == Some(true) { Some(render::rsx! { <> {" "}
    }) } else { None } }
  • } }) .collect::>() }
{ if community_info.you_are_moderator == Some(true) { Some(render::rsx! {

{lang.tr("community_add_moderator", None)}

{" "}
}) } else { None } }
})) } async fn handler_community_moderators_add( params: (i64,), ctx: Arc, req: hyper::Request, ) -> Result, crate::Error> { let (community_id,) = params; let (req_parts, body) = req.into_parts(); let cookies = get_cookie_map_for_headers(&req_parts.headers)?; #[derive(Deserialize)] struct ModeratorsAddParams { user: i64, } let body = hyper::body::to_bytes(body).await?; let body: ModeratorsAddParams = serde_urlencoded::from_bytes(&body)?; res_to_error( ctx.http_client .request(for_client( hyper::Request::put(format!( "{}/api/unstable/communities/{}/moderators/{}", ctx.backend_host, community_id, body.user, )) .body(Default::default())?, &req_parts.headers, &cookies, )?) .await?, ) .await?; Ok(hyper::Response::builder() .status(hyper::StatusCode::SEE_OTHER) .header( hyper::header::LOCATION, format!("/communities/{}/moderators", community_id), ) .body("Successfully added.".into())?) } async fn handler_community_moderators_remove( params: (i64,), ctx: Arc, req: hyper::Request, ) -> Result, crate::Error> { let (community_id,) = params; let (req_parts, body) = req.into_parts(); let cookies = get_cookie_map_for_headers(&req_parts.headers)?; #[derive(Deserialize)] struct ModeratorsRemoveParams { user: i64, } let body = hyper::body::to_bytes(body).await?; let body: ModeratorsRemoveParams = serde_urlencoded::from_bytes(&body)?; res_to_error( ctx.http_client .request(for_client( hyper::Request::delete(format!( "{}/api/unstable/communities/{}/moderators/{}", ctx.backend_host, community_id, body.user, )) .body(Default::default())?, &req_parts.headers, &cookies, )?) .await?, ) .await?; Ok(hyper::Response::builder() .status(hyper::StatusCode::SEE_OTHER) .header( hyper::header::LOCATION, format!("/communities/{}/moderators", community_id), ) .body("Successfully removed.".into())?) } async fn handler_community_post_approve( params: (i64, i64), ctx: Arc, req: hyper::Request, ) -> Result, crate::Error> { let (community_id, post_id) = params; let cookies = get_cookie_map_for_req(&req)?; res_to_error( ctx.http_client .request(for_client( hyper::Request::patch(format!( "{}/api/unstable/communities/{}/posts/{}", ctx.backend_host, community_id, post_id )) .body("{\"approved\": true}".into())?, req.headers(), &cookies, )?) .await?, ) .await?; Ok(hyper::Response::builder() .status(hyper::StatusCode::SEE_OTHER) .header(hyper::header::LOCATION, format!("/posts/{}", post_id)) .body("Successfully approved.".into())?) } async fn handler_community_post_unapprove( params: (i64, i64), ctx: Arc, req: hyper::Request, ) -> Result, crate::Error> { let (community_id, post_id) = params; let cookies = get_cookie_map_for_req(&req)?; res_to_error( ctx.http_client .request(for_client( hyper::Request::patch(format!( "{}/api/unstable/communities/{}/posts/{}", ctx.backend_host, community_id, post_id )) .body("{\"approved\": false}".into())?, req.headers(), &cookies, )?) .await?, ) .await?; Ok(hyper::Response::builder() .status(hyper::StatusCode::SEE_OTHER) .header(hyper::header::LOCATION, format!("/posts/{}", post_id)) .body("Successfully unapproved.".into())?) } async fn handler_community_unfollow( params: (i64,), ctx: Arc, req: hyper::Request, ) -> Result, crate::Error> { let (community_id,) = params; let cookies = get_cookie_map_for_req(&req)?; res_to_error( ctx.http_client .request(for_client( hyper::Request::post(format!( "{}/api/unstable/communities/{}/unfollow", ctx.backend_host, community_id )) .body(Default::default())?, req.headers(), &cookies, )?) .await?, ) .await?; Ok(hyper::Response::builder() .status(hyper::StatusCode::SEE_OTHER) .header( hyper::header::LOCATION, format!("/communities/{}", community_id), ) .body("Successfully unfollowed".into())?) } async fn page_community_new_post( params: (i64,), ctx: Arc, req: hyper::Request, ) -> Result, crate::Error> { let (community_id,) = params; let cookies = get_cookie_map_for_req(&req)?; page_community_new_post_inner(community_id, req.headers(), &cookies, ctx, None, None, None) .await } async fn page_community_new_post_inner( community_id: i64, headers: &hyper::header::HeaderMap, cookies: &CookieMap<'_>, ctx: Arc, display_error: Option, prev_values: Option<&HashMap, serde_json::Value>>, display_preview: Option<&str>, ) -> Result, crate::Error> { let base_data = fetch_base_data(&ctx.backend_host, &ctx.http_client, headers, &cookies).await?; let lang = crate::get_lang_for_headers(headers); let submit_url = format!("/communities/{}/new_post/submit", community_id); let title = lang.tr("post_new", None); let display_preview = display_preview.map(|x| ammonia::clean(&x)); Ok(html_response(render::html! {

{title.as_ref()}

{ display_error.map(|msg| { render::rsx! {
{msg}
} }) }
{ display_preview.as_deref().map(|html| { render::rsx! {
{render::raw!(html)}
} }) }
})) } async fn handler_communities_new_post_submit( params: (i64,), ctx: Arc, req: hyper::Request, ) -> Result, crate::Error> { let (community_id,) = params; let (req_parts, body) = req.into_parts(); let lang = crate::get_lang_for_headers(&req_parts.headers); let cookies = get_cookie_map_for_headers(&req_parts.headers)?; let content_type = req_parts .headers .get(hyper::header::CONTENT_TYPE) .ok_or_else(|| { crate::Error::InternalStr("missing content-type header in form submission".to_owned()) })?; let content_type = std::str::from_utf8(content_type.as_ref())?; let boundary = multer::parse_boundary(&content_type)?; let mut multipart = multer::Multipart::new(body, boundary); let mut body_values: HashMap, serde_json::Value> = HashMap::new(); { let mut error = None; loop { let field = multipart.next_field().await?; let field = match field { None => break, Some(field) => field, }; if field.name().is_none() { continue; } if field.name().unwrap() == "href_media" { use futures_util::StreamExt; let mut stream = field.peekable(); let first_chunk = std::pin::Pin::new(&mut stream).peek().await; let is_empty = match first_chunk { None => true, Some(Ok(chunk)) => chunk.is_empty(), Some(Err(err)) => { return Err(crate::Error::InternalStr(format!( "failed parsing form: {:?}", err ))); } }; if is_empty { continue; } if body_values.contains_key("href") && body_values["href"] != "" { error = Some(lang.tr("post_new_href_conflict", None).into_owned()); } else { match stream.get_ref().content_type() { None => { error = Some(lang.tr("post_new_missing_content_type", None).into_owned()); } Some(mime) => { println!("will upload media"); let res = res_to_error( ctx.http_client .request(for_client( hyper::Request::post(format!( "{}/api/unstable/media", ctx.backend_host, )) .header(hyper::header::CONTENT_TYPE, mime.as_ref()) .body(hyper::Body::wrap_stream(stream))?, &req_parts.headers, &cookies, )?) .await?, ) .await; match res { Err(crate::Error::RemoteError((_, message))) => { error = Some(message); } Err(other) => { return Err(other); } Ok(res) => { let res = hyper::body::to_bytes(res.into_body()).await?; let res: JustStringID = serde_json::from_slice(&res)?; body_values.insert( "href".into(), format!("local-media://{}", res.id).into(), ); } } println!("finished media upload"); } } } } else { let name = field.name().unwrap(); if name == "href" && body_values.contains_key("href") && body_values["href"] != "" { error = Some(lang.tr("post_new_href_conflict", None).into_owned()); } else { let name = name.to_owned(); let value = field.text().await?; body_values.insert(name.into(), value.into()); } } } if let Some(error) = error { return page_community_new_post_inner( community_id, &req_parts.headers, &cookies, ctx, Some(error), Some(&body_values), None, ) .await; } } if body_values.contains_key("preview") { let md = body_values .get("content_markdown") .and_then(|x| x.as_str()) .unwrap_or(""); let preview_res = res_to_error( ctx.http_client .request(for_client( hyper::Request::post(format!( "{}/api/unstable/misc/render_markdown", ctx.backend_host )) .body( serde_json::to_vec(&serde_json::json!({ "content_markdown": md }))?.into(), )?, &req_parts.headers, &cookies, )?) .await?, ) .await; return match preview_res { Ok(preview_res) => { let preview_res = hyper::body::to_bytes(preview_res.into_body()).await?; let preview_res: JustContentHTML = serde_json::from_slice(&preview_res)?; page_community_new_post_inner( community_id, &req_parts.headers, &cookies, ctx, None, Some(&body_values), Some(&preview_res.content_html), ) .await } Err(crate::Error::RemoteError((_, message))) => { page_community_new_post_inner( community_id, &req_parts.headers, &cookies, ctx, Some(message), Some(&body_values), None, ) .await } Err(other) => Err(other), }; } body_values.insert("community".into(), community_id.into()); if body_values.get("content_markdown").and_then(|x| x.as_str()) == Some("") { body_values.remove("content_markdown"); } if body_values.get("href").and_then(|x| x.as_str()) == Some("") { body_values.remove("href"); } let api_res = res_to_error( ctx.http_client .request(for_client( hyper::Request::post(format!("{}/api/unstable/posts", ctx.backend_host)) .body(serde_json::to_vec(&body_values)?.into())?, &req_parts.headers, &cookies, )?) .await?, ) .await; match api_res { Ok(api_res) => { #[derive(Deserialize)] struct PostsCreateResponse { id: i64, } let api_res = hyper::body::to_bytes(api_res.into_body()).await?; let api_res: PostsCreateResponse = serde_json::from_slice(&api_res)?; Ok(hyper::Response::builder() .status(hyper::StatusCode::SEE_OTHER) .header(hyper::header::LOCATION, format!("/posts/{}", api_res.id)) .body("Successfully posted.".into())?) } Err(crate::Error::RemoteError((_, message))) => { page_community_new_post_inner( community_id, &req_parts.headers, &cookies, ctx, Some(message), Some(&body_values), None, ) .await } Err(other) => Err(other), } } pub fn route_communities() -> crate::RouteNode<()> { crate::RouteNode::new() .with_handler_async("GET", page_communities) .with_child_parse::( crate::RouteNode::new() .with_handler_async("GET", page_community) .with_child( "edit", crate::RouteNode::new() .with_handler_async("GET", page_community_edit) .with_child( "submit", crate::RouteNode::new() .with_handler_async("POST", handler_communities_edit_submit), ), ) .with_child( "follow", crate::RouteNode::new().with_handler_async("POST", handler_community_follow), ) .with_child( "moderators", crate::RouteNode::new() .with_handler_async("GET", page_community_moderators) .with_child( "add", crate::RouteNode::new() .with_handler_async("POST", handler_community_moderators_add), ) .with_child( "remove", crate::RouteNode::new() .with_handler_async("POST", handler_community_moderators_remove), ), ) .with_child( "posts", crate::RouteNode::new().with_child_parse::( crate::RouteNode::new() .with_child( "approve", crate::RouteNode::new() .with_handler_async("POST", handler_community_post_approve), ) .with_child( "unapprove", crate::RouteNode::new() .with_handler_async("POST", handler_community_post_unapprove), ), ), ) .with_child( "unfollow", crate::RouteNode::new().with_handler_async("POST", handler_community_unfollow), ) .with_child( "new_post", crate::RouteNode::new() .with_handler_async("GET", page_community_new_post) .with_child( "submit", crate::RouteNode::new() .with_handler_async("POST", handler_communities_new_post_submit), ), ), ) }