From 251e0d3b82fd21688c0d0cc2566454a1a9111a78 Mon Sep 17 00:00:00 2001 From: Felix Ableitner Date: Tue, 20 Jul 2021 09:00:20 +0200 Subject: [PATCH 1/2] Move resolving of activitypub objects to separate api endpoint (fixes #1584) --- Cargo.lock | 4 + crates/api/src/lib.rs | 3 + crates/api/src/site.rs | 20 ++- crates/api_common/src/lib.rs | 18 --- crates/api_common/src/site.rs | 14 ++ crates/apub/src/activities/comment/mod.rs | 5 +- crates/apub/src/fetcher/search.rs | 149 +++++++++++----------- crates/apub_lib/Cargo.toml | 3 + crates/apub_lib/src/lib.rs | 2 + crates/apub_lib/src/webfinger.rs | 72 +++++++++++ crates/routes/Cargo.toml | 1 + crates/routes/src/webfinger.rs | 9 +- crates/websocket/src/lib.rs | 1 + src/api_routes.rs | 5 + 14 files changed, 202 insertions(+), 104 deletions(-) create mode 100644 crates/apub_lib/src/webfinger.rs diff --git a/Cargo.lock b/Cargo.lock index 4cdcc47f6..624897b38 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1736,10 +1736,13 @@ name = "lemmy_apub_lib" version = "0.11.3" dependencies = [ "activitystreams", + "anyhow", "async-trait", "lemmy_apub_lib_derive", "lemmy_utils", "lemmy_websocket", + "log", + "reqwest", "serde", "serde_json", "url", @@ -1836,6 +1839,7 @@ dependencies = [ "diesel", "lazy_static", "lemmy_api_common", + "lemmy_apub_lib", "lemmy_db_queries", "lemmy_db_schema", "lemmy_db_views", diff --git a/crates/api/src/lib.rs b/crates/api/src/lib.rs index bf3a813b3..83d98bde5 100644 --- a/crates/api/src/lib.rs +++ b/crates/api/src/lib.rs @@ -87,6 +87,9 @@ pub async fn match_websocket_operation( do_websocket_operation::(context, id, op, data).await } UserOperation::Search => do_websocket_operation::(context, id, op, data).await, + UserOperation::ResolveObject => { + do_websocket_operation::(context, id, op, data).await + } UserOperation::TransferCommunity => { do_websocket_operation::(context, id, op, data).await } diff --git a/crates/api/src/site.rs b/crates/api/src/site.rs index 221b9870f..315cf72da 100644 --- a/crates/api/src/site.rs +++ b/crates/api/src/site.rs @@ -50,7 +50,6 @@ use lemmy_utils::{ LemmyError, }; use lemmy_websocket::LemmyContext; -use log::debug; #[async_trait::async_trait(?Send)] impl Perform for GetModlog { @@ -143,11 +142,6 @@ impl Perform for Search { ) -> Result { let data: &Search = self; - match search_by_apub_id(&data.q, context).await { - Ok(r) => return Ok(r), - Err(e) => debug!("Failed to resolve search query as activitypub ID: {}", e), - } - let local_user_view = get_local_user_view_from_jwt_opt(&data.auth, context.pool()).await?; let show_nsfw = local_user_view.as_ref().map(|t| t.local_user.show_nsfw); @@ -372,6 +366,20 @@ impl Perform for Search { } } +#[async_trait::async_trait(?Send)] +impl Perform for ResolveObject { + type Response = ResolveObjectResponse; + + async fn perform( + &self, + context: &Data, + _websocket_id: Option, + ) -> Result { + let local_user_view = get_local_user_view_from_jwt_opt(&self.auth, context.pool()).await?; + search_by_apub_id(&self.q, local_user_view, context).await + } +} + #[async_trait::async_trait(?Send)] impl Perform for TransferSite { type Response = GetSiteResponse; diff --git a/crates/api_common/src/lib.rs b/crates/api_common/src/lib.rs index e7d5181bc..a79e842ee 100644 --- a/crates/api_common/src/lib.rs +++ b/crates/api_common/src/lib.rs @@ -46,26 +46,8 @@ use lemmy_utils::{ LemmyError, }; use log::error; -use serde::{Deserialize, Serialize}; use url::Url; -#[derive(Serialize, Deserialize, Debug)] -pub struct WebFingerLink { - pub rel: Option, - #[serde(rename(serialize = "type", deserialize = "type"))] - pub type_: Option, - pub href: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub template: Option, -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct WebFingerResponse { - pub subject: String, - pub aliases: Vec, - pub links: Vec, -} - pub async fn blocking(pool: &DbPool, f: F) -> Result where F: FnOnce(&diesel::PgConnection) -> T + Send + 'static, diff --git a/crates/api_common/src/site.rs b/crates/api_common/src/site.rs index af63854d3..ed6211781 100644 --- a/crates/api_common/src/site.rs +++ b/crates/api_common/src/site.rs @@ -50,6 +50,20 @@ pub struct SearchResponse { pub users: Vec, } +#[derive(Deserialize, Debug)] +pub struct ResolveObject { + pub q: String, + pub auth: Option, +} + +#[derive(Serialize, Debug)] +pub enum ResolveObjectResponse { + Comment(CommentView), + Post(PostView), + Community(CommunityView), + Person(PersonViewSafe), +} + #[derive(Deserialize)] pub struct GetModlog { pub mod_person_id: Option, diff --git a/crates/apub/src/activities/comment/mod.rs b/crates/apub/src/activities/comment/mod.rs index 228a5f804..44b8d245a 100644 --- a/crates/apub/src/activities/comment/mod.rs +++ b/crates/apub/src/activities/comment/mod.rs @@ -5,7 +5,8 @@ use activitystreams::{ }; use anyhow::anyhow; use itertools::Itertools; -use lemmy_api_common::{blocking, send_local_notifs, WebFingerResponse}; +use lemmy_api_common::{blocking, send_local_notifs}; +use lemmy_apub_lib::webfinger::WebfingerResponse; use lemmy_db_queries::{Crud, DbPool}; use lemmy_db_schema::{ source::{comment::Comment, community::Community, person::Person, post::Post}, @@ -128,7 +129,7 @@ async fn fetch_webfinger_url(mention: &MentionData, client: &Client) -> Result, context: &LemmyContext, -) -> Result { - // Parse the shorthand query url - let query_url = if query.contains('@') { - debug!("Search for {}", query); - let split = query.split('@').collect::>(); - - // Person type will look like ['', username, instance] - // Community will look like [!community, instance] - let (name, instance) = if split.len() == 3 { - (format!("/u/{}", split[1]), split[2]) - } else if split.len() == 2 { - if split[0].contains('!') { - let split2 = split[0].split('!').collect::>(); - (format!("/c/{}", split2[1]), split[1]) - } else { - return Err(anyhow!("Invalid search query: {}", query).into()); +) -> Result { + let query_url = match Url::parse(query) { + Ok(u) => u, + Err(_) => { + let (kind, name) = query.split_at(1); + let kind = match kind { + "@" => WebfingerType::Person, + "!" => WebfingerType::Group, + _ => return Err(anyhow!("invalid query").into()), + }; + // remote actor, use webfinger to resolve url + if name.contains('@') { + let (name, domain) = name.splitn(2, '@').collect_tuple().expect("invalid query"); + webfinger_resolve_actor(name, domain, kind, context.client()).await? } - } else { - return Err(anyhow!("Invalid search query: {}", query).into()); - }; - - let url = format!( - "{}://{}{}", - Settings::get().get_protocol_string(), - instance, - name - ); - Url::parse(&url)? - } else { - Url::parse(query)? + // local actor, read from database and return + else { + let name: String = name.into(); + return match kind { + WebfingerType::Group => { + let res = blocking(context.pool(), move |conn| { + let community = Community::read_from_name(conn, &name)?; + CommunityView::read(conn, community.id, local_user_view.map(|l| l.person.id)) + }) + .await??; + Ok(ResolveObjectResponse::Community(res)) + } + WebfingerType::Person => { + let res = blocking(context.pool(), move |conn| { + let person = Person::find_by_name(conn, &name)?; + PersonViewSafe::read(conn, person.id) + }) + .await??; + Ok(ResolveObjectResponse::Person(res)) + } + }; + } + } }; - let recursion_counter = &mut 0; + let request_counter = &mut 0; + // this does a fetch (even for local objects), just to determine its type and fetch it again + // below. we need to fix this when rewriting the fetcher. let fetch_response = - fetch_remote_object::(context.client(), &query_url, recursion_counter) + fetch_remote_object::(context.client(), &query_url, request_counter) .await; if is_deleted(&fetch_response) { delete_object_locally(&query_url, context).await?; + return Err(anyhow!("Object was deleted").into()); } // Necessary because we get a stack overflow using FetchError let fet_res = fetch_response.map_err(|e| LemmyError::from(e.inner))?; - build_response(fet_res, query_url, recursion_counter, context).await + build_response(fet_res, query_url, request_counter, context).await } async fn build_response( @@ -105,58 +119,45 @@ async fn build_response( query_url: Url, recursion_counter: &mut i32, context: &LemmyContext, -) -> Result { - let mut response = SearchResponse { - type_: SearchType::All.to_string(), - comments: vec![], - posts: vec![], - communities: vec![], - users: vec![], - }; - - match fetch_response { +) -> Result { + use ResolveObjectResponse as ROR; + Ok(match fetch_response { SearchAcceptedObjects::Person(p) => { - let person_id = p.id(&query_url)?; - let person = get_or_fetch_and_upsert_person(person_id, context, recursion_counter).await?; + let person_uri = p.id(&query_url)?; - response.users = vec![ + let person = get_or_fetch_and_upsert_person(person_uri, context, recursion_counter).await?; + ROR::Person( blocking(context.pool(), move |conn| { PersonViewSafe::read(conn, person.id) }) .await??, - ]; + ) } SearchAcceptedObjects::Group(g) => { let community_uri = g.id(&query_url)?; let community = get_or_fetch_and_upsert_community(community_uri, context, recursion_counter).await?; - - response.communities = vec![ + ROR::Community( blocking(context.pool(), move |conn| { CommunityView::read(conn, community.id, None) }) .await??, - ]; + ) } SearchAcceptedObjects::Page(p) => { let p = Post::from_apub(&p, context, &query_url, recursion_counter).await?; - - response.posts = - vec![blocking(context.pool(), move |conn| PostView::read(conn, p.id, None)).await??]; + ROR::Post(blocking(context.pool(), move |conn| PostView::read(conn, p.id, None)).await??) } SearchAcceptedObjects::Comment(c) => { let c = Comment::from_apub(&c, context, &query_url, recursion_counter).await?; - - response.comments = vec![ + ROR::Comment( blocking(context.pool(), move |conn| { CommentView::read(conn, c.id, None) }) .await??, - ]; + ) } - }; - - Ok(response) + }) } async fn delete_object_locally(query_url: &Url, context: &LemmyContext) -> Result<(), LemmyError> { @@ -194,5 +195,5 @@ async fn delete_object_locally(query_url: &Url, context: &LemmyContext) -> Resul .await??; } } - Err(anyhow!("Object was deleted").into()) + Ok(()) } diff --git a/crates/apub_lib/Cargo.toml b/crates/apub_lib/Cargo.toml index 2593cd94e..7ea84c1f8 100644 --- a/crates/apub_lib/Cargo.toml +++ b/crates/apub_lib/Cargo.toml @@ -14,3 +14,6 @@ serde = { version = "1.0.127", features = ["derive"] } async-trait = "0.1.51" url = { version = "2.2.2", features = ["serde"] } serde_json = { version = "1.0.66", features = ["preserve_order"] } +anyhow = "1.0.41" +reqwest = { version = "0.11.4", features = ["json"] } +log = "0.4.14" diff --git a/crates/apub_lib/src/lib.rs b/crates/apub_lib/src/lib.rs index cc88b79c9..846666ed2 100644 --- a/crates/apub_lib/src/lib.rs +++ b/crates/apub_lib/src/lib.rs @@ -6,6 +6,8 @@ use lemmy_utils::LemmyError; use lemmy_websocket::LemmyContext; use url::Url; +pub mod webfinger; + pub trait ActivityFields { fn id_unchecked(&self) -> &Url; fn actor(&self) -> &Url; diff --git a/crates/apub_lib/src/webfinger.rs b/crates/apub_lib/src/webfinger.rs new file mode 100644 index 000000000..ebd49ea67 --- /dev/null +++ b/crates/apub_lib/src/webfinger.rs @@ -0,0 +1,72 @@ +use anyhow::anyhow; +use lemmy_utils::{ + request::{retry, RecvError}, + settings::structs::Settings, + LemmyError, +}; +use log::debug; +use reqwest::Client; +use serde::{Deserialize, Serialize}; +use url::Url; + +#[derive(Serialize, Deserialize, Debug)] +pub struct WebfingerLink { + pub rel: Option, + #[serde(rename(serialize = "type", deserialize = "type"))] + pub type_: Option, + pub href: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub template: Option, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct WebfingerResponse { + pub subject: String, + pub aliases: Vec, + pub links: Vec, +} + +pub enum WebfingerType { + Person, + Group, +} + +/// Turns a person id like `@name@example.com` into an apub ID, like `https://example.com/user/name`, +/// using webfinger. +pub async fn webfinger_resolve_actor( + name: &str, + domain: &str, + webfinger_type: WebfingerType, + client: &Client, +) -> Result { + let webfinger_type = match webfinger_type { + WebfingerType::Person => "acct", + WebfingerType::Group => "group", + }; + let fetch_url = format!( + "{}://{}/.well-known/webfinger?resource={}:{}@{}", + Settings::get().get_protocol_string(), + domain, + webfinger_type, + name, + domain + ); + debug!("Fetching webfinger url: {}", &fetch_url); + + let response = retry(|| client.get(&fetch_url).send()).await?; + + let res: WebfingerResponse = response + .json() + .await + .map_err(|e| RecvError(e.to_string()))?; + + let link = res + .links + .iter() + .find(|l| l.type_.eq(&Some("application/activity+json".to_string()))) + .ok_or_else(|| anyhow!("No application/activity+json link found."))?; + link + .href + .to_owned() + .ok_or_else(|| anyhow!("No href found.").into()) +} diff --git a/crates/routes/Cargo.toml b/crates/routes/Cargo.toml index a29172ac2..e47e7a36e 100644 --- a/crates/routes/Cargo.toml +++ b/crates/routes/Cargo.toml @@ -16,6 +16,7 @@ lemmy_db_views = { version = "=0.11.3", path = "../db_views" } lemmy_db_views_actor = { version = "=0.11.3", path = "../db_views_actor" } lemmy_db_schema = { version = "=0.11.3", path = "../db_schema" } lemmy_api_common = { version = "=0.11.3", path = "../api_common" } +lemmy_apub_lib = { version = "=0.11.3", path = "../apub_lib" } diesel = "1.4.7" actix = "0.12.0" actix-web = { version = "4.0.0-beta.8", default-features = false, features = ["rustls"] } diff --git a/crates/routes/src/webfinger.rs b/crates/routes/src/webfinger.rs index 5d4be74c4..677321887 100644 --- a/crates/routes/src/webfinger.rs +++ b/crates/routes/src/webfinger.rs @@ -1,6 +1,7 @@ use actix_web::{error::ErrorBadRequest, web::Query, *}; use anyhow::anyhow; -use lemmy_api_common::{blocking, WebFingerLink, WebFingerResponse}; +use lemmy_api_common::blocking; +use lemmy_apub_lib::webfinger::{WebfingerLink, WebfingerResponse}; use lemmy_db_queries::source::{community::Community_, person::Person_}; use lemmy_db_schema::source::{community::Community, person::Person}; use lemmy_utils::{ @@ -68,17 +69,17 @@ async fn get_webfinger_response( return Err(ErrorBadRequest(LemmyError::from(anyhow!("not_found")))); }; - let json = WebFingerResponse { + let json = WebfingerResponse { subject: info.resource.to_owned(), aliases: vec![url.to_owned().into()], links: vec![ - WebFingerLink { + WebfingerLink { rel: Some("http://webfinger.net/rel/profile-page".to_string()), type_: Some("text/html".to_string()), href: Some(url.to_owned().into()), template: None, }, - WebFingerLink { + WebfingerLink { rel: Some("self".to_string()), type_: Some("application/activity+json".to_string()), href: Some(url.into()), diff --git a/crates/websocket/src/lib.rs b/crates/websocket/src/lib.rs index fbb2bb512..d501e9611 100644 --- a/crates/websocket/src/lib.rs +++ b/crates/websocket/src/lib.rs @@ -110,6 +110,7 @@ pub enum UserOperation { AddAdmin, BanPerson, Search, + ResolveObject, MarkAllAsRead, SaveUserSettings, TransferCommunity, diff --git a/src/api_routes.rs b/src/api_routes.rs index feaf0e235..1d76af002 100644 --- a/src/api_routes.rs +++ b/src/api_routes.rs @@ -33,6 +33,11 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) { .wrap(rate_limit.message()) .route(web::get().to(route_get::)), ) + .service( + web::resource("/resolve_object") + .wrap(rate_limit.message()) + .route(web::get().to(route_get::)), + ) // Community .service( web::resource("/community") From c23e7cc20d445396c5a524febd2f7925e4a205a4 Mon Sep 17 00:00:00 2001 From: Dessalines Date: Mon, 23 Aug 2021 11:25:39 -0400 Subject: [PATCH 2/2] Fixing ResolveObject API and unit tests (#1713) --- api_tests/package.json | 2 +- api_tests/src/comment.spec.ts | 59 ++++++++++------------ api_tests/src/community.spec.ts | 34 ++++++------- api_tests/src/follow.spec.ts | 6 +-- api_tests/src/post.spec.ts | 82 +++++++++++++------------------ api_tests/src/shared.ts | 66 ++++++++++--------------- api_tests/src/user.spec.ts | 10 ++-- api_tests/yarn.lock | 8 +-- crates/api/src/site.rs | 5 +- crates/api_common/src/site.rs | 12 ++--- crates/apub/src/fetcher/search.rs | 47 ++++++++++++------ 11 files changed, 153 insertions(+), 178 deletions(-) diff --git a/api_tests/package.json b/api_tests/package.json index e8e4cc091..33bb3e0b3 100644 --- a/api_tests/package.json +++ b/api_tests/package.json @@ -16,7 +16,7 @@ "eslint": "^7.30.0", "eslint-plugin-jane": "^9.0.3", "jest": "^27.0.6", - "lemmy-js-client": "0.11.4-rc.9", + "lemmy-js-client": "0.11.4-rc.14", "node-fetch": "^2.6.1", "prettier": "^2.3.2", "ts-jest": "^27.0.3", diff --git a/api_tests/src/comment.spec.ts b/api_tests/src/comment.spec.ts index 309cfd133..b1739d4b4 100644 --- a/api_tests/src/comment.spec.ts +++ b/api_tests/src/comment.spec.ts @@ -6,16 +6,16 @@ import { setupLogins, createPost, getPost, - searchComment, + resolveComment, likeComment, followBeta, - searchForBetaCommunity, + resolveBetaCommunity, createComment, editComment, deleteComment, removeComment, getMentions, - searchPost, + resolvePost, unfollowRemotes, createCommunity, registerUser, @@ -31,10 +31,10 @@ beforeAll(async () => { await setupLogins(); await followBeta(alpha); await followBeta(gamma); - let search = await searchForBetaCommunity(alpha); + let betaCommunity = (await resolveBetaCommunity(alpha)).community; postRes = await createPost( alpha, - search.communities.find(c => c.community.local == false).community.id + betaCommunity.community.id ); }); @@ -65,8 +65,7 @@ test('Create a comment', async () => { expect(commentRes.comment_view.counts.score).toBe(1); // Make sure that comment is liked on beta - let searchBeta = await searchComment(beta, commentRes.comment_view.comment); - let betaComment = searchBeta.comments[0]; + let betaComment = (await resolveComment(beta, commentRes.comment_view.comment)).comment; expect(betaComment).toBeDefined(); expect(betaComment.community.local).toBe(true); expect(betaComment.creator.local).toBe(false); @@ -82,8 +81,8 @@ test('Create a comment in a non-existent post', async () => { test('Update a comment', async () => { let commentRes = await createComment(alpha, postRes.post_view.post.id); // Federate the comment first - let searchBeta = await searchComment(beta, commentRes.comment_view.comment); - assertCommentFederation(searchBeta.comments[0], commentRes.comment_view); + let betaComment = (await resolveComment(beta, commentRes.comment_view.comment)).comment; + assertCommentFederation(betaComment, commentRes.comment_view); let updateCommentRes = await editComment( alpha, @@ -96,12 +95,12 @@ test('Update a comment', async () => { expect(updateCommentRes.comment_view.creator.local).toBe(true); // Make sure that post is updated on beta - let searchBetaUpdated = await searchComment( + let betaCommentUpdated = (await resolveComment( beta, commentRes.comment_view.comment - ); + )).comment; assertCommentFederation( - searchBetaUpdated.comments[0], + betaCommentUpdated, updateCommentRes.comment_view ); }); @@ -118,9 +117,8 @@ test('Delete a comment', async () => { expect(deleteCommentRes.comment_view.comment.content).toBe(""); // Make sure that comment is undefined on beta - let searchBeta = await searchComment(beta, commentRes.comment_view.comment); - let betaComment = searchBeta.comments[0]; - expect(betaComment).toBeUndefined(); + let betaCommentRes: any = await resolveComment(beta, commentRes.comment_view.comment); + expect(betaCommentRes).toStrictEqual({ error: 'couldnt_find_object' }); let undeleteCommentRes = await deleteComment( alpha, @@ -130,11 +128,10 @@ test('Delete a comment', async () => { expect(undeleteCommentRes.comment_view.comment.deleted).toBe(false); // Make sure that comment is undeleted on beta - let searchBeta2 = await searchComment(beta, commentRes.comment_view.comment); - let betaComment2 = searchBeta2.comments[0]; + let betaComment2 = (await resolveComment(beta, commentRes.comment_view.comment)).comment; expect(betaComment2.comment.deleted).toBe(false); assertCommentFederation( - searchBeta2.comments[0], + betaComment2, undeleteCommentRes.comment_view ); }); @@ -144,8 +141,8 @@ test('Remove a comment from admin and community on the same instance', async () // Get the id for beta let betaCommentId = ( - await searchComment(beta, commentRes.comment_view.comment) - ).comments[0].comment.id; + await resolveComment(beta, commentRes.comment_view.comment) + ).comment.comment.id; // The beta admin removes it (the community lives on beta) let removeCommentRes = await removeComment(beta, true, betaCommentId); @@ -185,8 +182,7 @@ test('Remove a comment from admin and community on different instance', async () expect(commentRes.comment_view.comment.content).toBeDefined(); // Beta searches that to cache it, then removes it - let searchBeta = await searchComment(beta, commentRes.comment_view.comment); - let betaComment = searchBeta.comments[0]; + let betaComment = (await resolveComment(beta, commentRes.comment_view.comment)).comment; let removeCommentRes = await removeComment( beta, true, @@ -206,8 +202,7 @@ test('Unlike a comment', async () => { expect(unlike.comment_view.counts.score).toBe(0); // Make sure that post is unliked on beta - let searchBeta = await searchComment(beta, commentRes.comment_view.comment); - let betaComment = searchBeta.comments[0]; + let betaComment = (await resolveComment(beta, commentRes.comment_view.comment)).comment; expect(betaComment).toBeDefined(); expect(betaComment.community.local).toBe(true); expect(betaComment.creator.local).toBe(false); @@ -218,8 +213,7 @@ test('Federated comment like', async () => { let commentRes = await createComment(alpha, postRes.post_view.post.id); // Find the comment on beta - let searchBeta = await searchComment(beta, commentRes.comment_view.comment); - let betaComment = searchBeta.comments[0]; + let betaComment = (await resolveComment(beta, commentRes.comment_view.comment)).comment; let like = await likeComment(beta, 1, betaComment.comment); expect(like.comment_view.counts.score).toBe(2); @@ -232,8 +226,7 @@ test('Federated comment like', async () => { test('Reply to a comment', async () => { // Create a comment on alpha, find it on beta let commentRes = await createComment(alpha, postRes.post_view.post.id); - let searchBeta = await searchComment(beta, commentRes.comment_view.comment); - let betaComment = searchBeta.comments[0]; + let betaComment = (await resolveComment(beta, commentRes.comment_view.comment)).comment; // find that comment id on beta @@ -287,8 +280,8 @@ test('Mention beta', async () => { test('Comment Search', async () => { let commentRes = await createComment(alpha, postRes.post_view.post.id); - let searchBeta = await searchComment(beta, commentRes.comment_view.comment); - assertCommentFederation(searchBeta.comments[0], commentRes.comment_view); + let betaComment = (await resolveComment(beta, commentRes.comment_view.comment)).comment; + assertCommentFederation(betaComment, commentRes.comment_view); }); test('A and G subscribe to B (center) A posts, G mentions B, it gets announced to A', async () => { @@ -297,8 +290,7 @@ test('A and G subscribe to B (center) A posts, G mentions B, it gets announced t expect(alphaPost.post_view.community.local).toBe(true); // Make sure gamma sees it - let search = await searchPost(gamma, alphaPost.post_view.post); - let gammaPost = search.posts[0]; + let gammaPost = (await resolvePost(gamma, alphaPost.post_view.post)).post; let commentContent = 'A jest test federated comment announce, lets mention @lemmy_beta@lemmy-beta:8551'; @@ -379,8 +371,7 @@ test('Fetch in_reply_tos: A is unsubbed from B, B makes a post, and some embedde expect(updateRes.comment_view.comment.content).toBe(updatedCommentContent); // Get the post from alpha - let search = await searchPost(alpha, postRes.post_view.post); - let alphaPostB = search.posts[0]; + let alphaPostB = (await resolvePost(alpha, postRes.post_view.post)).post; let alphaPost = await getPost(alpha, alphaPostB.post.id); expect(alphaPost.post_view.post.name).toBeDefined(); diff --git a/api_tests/src/community.spec.ts b/api_tests/src/community.spec.ts index 3d4a3a350..cdbb99e00 100644 --- a/api_tests/src/community.spec.ts +++ b/api_tests/src/community.spec.ts @@ -3,7 +3,7 @@ import { alpha, beta, setupLogins, - searchForCommunity, + resolveCommunity, createCommunity, deleteCommunity, removeCommunity, @@ -47,9 +47,8 @@ test('Create community', async () => { // Cache the community on beta, make sure it has the other fields let searchShort = `!${prevName}@lemmy-alpha:8541`; - let search = await searchForCommunity(beta, searchShort); - let communityOnBeta = search.communities[0]; - assertCommunityFederation(communityOnBeta, communityRes.community_view); + let betaCommunity = (await resolveCommunity(beta, searchShort)).community; + assertCommunityFederation(betaCommunity, communityRes.community_view); }); test('Delete community', async () => { @@ -57,15 +56,14 @@ test('Delete community', async () => { // Cache the community on Alpha let searchShort = `!${communityRes.community_view.community.name}@lemmy-beta:8551`; - let search = await searchForCommunity(alpha, searchShort); - let communityOnAlpha = search.communities[0]; - assertCommunityFederation(communityOnAlpha, communityRes.community_view); + let alphaCommunity = (await resolveCommunity(alpha, searchShort)).community; + assertCommunityFederation(alphaCommunity, communityRes.community_view); // Follow the community from alpha let follow = await followCommunity( alpha, true, - communityOnAlpha.community.id + alphaCommunity.community.id ); // Make sure the follow response went through @@ -82,7 +80,7 @@ test('Delete community', async () => { // Make sure it got deleted on A let communityOnAlphaDeleted = await getCommunity( alpha, - communityOnAlpha.community.id + alphaCommunity.community.id ); expect(communityOnAlphaDeleted.community_view.community.deleted).toBe(true); @@ -97,7 +95,7 @@ test('Delete community', async () => { // Make sure it got undeleted on A let communityOnAlphaUnDeleted = await getCommunity( alpha, - communityOnAlpha.community.id + alphaCommunity.community.id ); expect(communityOnAlphaUnDeleted.community_view.community.deleted).toBe( false @@ -109,15 +107,14 @@ test('Remove community', async () => { // Cache the community on Alpha let searchShort = `!${communityRes.community_view.community.name}@lemmy-beta:8551`; - let search = await searchForCommunity(alpha, searchShort); - let communityOnAlpha = search.communities[0]; - assertCommunityFederation(communityOnAlpha, communityRes.community_view); + let alphaCommunity = (await resolveCommunity(alpha, searchShort)).community; + assertCommunityFederation(alphaCommunity, communityRes.community_view); // Follow the community from alpha let follow = await followCommunity( alpha, true, - communityOnAlpha.community.id + alphaCommunity.community.id ); // Make sure the follow response went through @@ -134,7 +131,7 @@ test('Remove community', async () => { // Make sure it got Removed on A let communityOnAlphaRemoved = await getCommunity( alpha, - communityOnAlpha.community.id + alphaCommunity.community.id ); expect(communityOnAlphaRemoved.community_view.community.removed).toBe(true); @@ -149,7 +146,7 @@ test('Remove community', async () => { // Make sure it got undeleted on A let communityOnAlphaUnRemoved = await getCommunity( alpha, - communityOnAlpha.community.id + alphaCommunity.community.id ); expect(communityOnAlphaUnRemoved.community_view.community.removed).toBe( false @@ -161,7 +158,6 @@ test('Search for beta community', async () => { expect(communityRes.community_view.community.name).toBeDefined(); let searchShort = `!${communityRes.community_view.community.name}@lemmy-beta:8551`; - let search = await searchForCommunity(alpha, searchShort); - let communityOnAlpha = search.communities[0]; - assertCommunityFederation(communityOnAlpha, communityRes.community_view); + let alphaCommunity = (await resolveCommunity(alpha, searchShort)).community; + assertCommunityFederation(alphaCommunity, communityRes.community_view); }); diff --git a/api_tests/src/follow.spec.ts b/api_tests/src/follow.spec.ts index 369a772a1..078c382c2 100644 --- a/api_tests/src/follow.spec.ts +++ b/api_tests/src/follow.spec.ts @@ -2,7 +2,7 @@ jest.setTimeout(120000); import { alpha, setupLogins, - searchForBetaCommunity, + resolveBetaCommunity, followCommunity, unfollowRemotes, getSite, @@ -17,11 +17,11 @@ afterAll(async () => { }); test('Follow federated community', async () => { - let search = await searchForBetaCommunity(alpha); // TODO sometimes this is returning null? + let betaCommunity = (await resolveBetaCommunity(alpha)).community; let follow = await followCommunity( alpha, true, - search.communities[0].community.id + betaCommunity.community.id ); // Make sure the follow response went through diff --git a/api_tests/src/post.spec.ts b/api_tests/src/post.spec.ts index c5630f336..8836a2c76 100644 --- a/api_tests/src/post.spec.ts +++ b/api_tests/src/post.spec.ts @@ -10,16 +10,16 @@ import { editPost, stickyPost, lockPost, - searchPost, + resolvePost, likePost, followBeta, - searchForBetaCommunity, + resolveBetaCommunity, createComment, deletePost, removePost, getPost, unfollowRemotes, - searchForUser, + resolvePerson, banPersonFromSite, searchPostLocal, followCommunity, @@ -31,8 +31,8 @@ let betaCommunity: CommunityView; beforeAll(async () => { await setupLogins(); - let search = await searchForBetaCommunity(alpha); - betaCommunity = search.communities[0]; + betaCommunity = (await resolveBetaCommunity(alpha)).community; + expect(betaCommunity).toBeDefined(); await unfollows(); }); @@ -71,8 +71,7 @@ test('Create a post', async () => { expect(postRes.post_view.counts.score).toBe(1); // Make sure that post is liked on beta - let searchBeta = await searchPost(beta, postRes.post_view.post); - let betaPost = searchBeta.posts[0]; + let betaPost = (await resolvePost(beta, postRes.post_view.post)).post; expect(betaPost).toBeDefined(); expect(betaPost.community.local).toBe(true); @@ -81,12 +80,12 @@ test('Create a post', async () => { assertPostFederation(betaPost, postRes.post_view); // Delta only follows beta, so it should not see an alpha ap_id - let searchDelta = await searchPost(delta, postRes.post_view.post); - expect(searchDelta.posts[0]).toBeUndefined(); + let deltaPost = (await resolvePost(delta, postRes.post_view.post)).post; + expect(deltaPost).toBeUndefined(); // Epsilon has alpha blocked, it should not see the alpha post - let searchEpsilon = await searchPost(epsilon, postRes.post_view.post); - expect(searchEpsilon.posts[0]).toBeUndefined(); + let epsilonPost = (await resolvePost(epsilon, postRes.post_view.post)).post; + expect(epsilonPost).toBeUndefined(); }); test('Create a post in a non-existent community', async () => { @@ -104,8 +103,7 @@ test('Unlike a post', async () => { expect(unlike2.post_view.counts.score).toBe(0); // Make sure that post is unliked on beta - let searchBeta = await searchPost(beta, postRes.post_view.post); - let betaPost = searchBeta.posts[0]; + let betaPost = (await resolvePost(beta, postRes.post_view.post)).post; expect(betaPost).toBeDefined(); expect(betaPost.community.local).toBe(true); expect(betaPost.creator.local).toBe(false); @@ -123,8 +121,7 @@ test('Update a post', async () => { expect(updatedPost.post_view.creator.local).toBe(true); // Make sure that post is updated on beta - let searchBeta = await searchPost(beta, postRes.post_view.post); - let betaPost = searchBeta.posts[0]; + let betaPost = (await resolvePost(beta, postRes.post_view.post)).post; expect(betaPost.community.local).toBe(true); expect(betaPost.creator.local).toBe(false); expect(betaPost.post.name).toBe(updatedName); @@ -142,8 +139,7 @@ test('Sticky a post', async () => { expect(stickiedPostRes.post_view.post.stickied).toBe(true); // Make sure that post is stickied on beta - let searchBeta = await searchPost(beta, postRes.post_view.post); - let betaPost = searchBeta.posts[0]; + let betaPost = (await resolvePost(beta, postRes.post_view.post)).post; expect(betaPost.community.local).toBe(true); expect(betaPost.creator.local).toBe(false); expect(betaPost.post.stickied).toBe(true); @@ -153,18 +149,15 @@ test('Sticky a post', async () => { expect(unstickiedPost.post_view.post.stickied).toBe(false); // Make sure that post is unstickied on beta - let searchBeta2 = await searchPost(beta, postRes.post_view.post); - let betaPost2 = searchBeta2.posts[0]; + let betaPost2 = (await resolvePost(beta, postRes.post_view.post)).post; expect(betaPost2.community.local).toBe(true); expect(betaPost2.creator.local).toBe(false); expect(betaPost2.post.stickied).toBe(false); // Make sure that gamma cannot sticky the post on beta - let searchGamma = await searchPost(gamma, postRes.post_view.post); - let gammaPost = searchGamma.posts[0]; + let gammaPost = (await resolvePost(gamma, postRes.post_view.post)).post; let gammaTrySticky = await stickyPost(gamma, true, gammaPost.post); - let searchBeta3 = await searchPost(beta, postRes.post_view.post); - let betaPost3 = searchBeta3.posts[0]; + let betaPost3 = (await resolvePost(beta, postRes.post_view.post)).post; expect(gammaTrySticky.post_view.post.stickied).toBe(true); expect(betaPost3.post.stickied).toBe(false); }); @@ -174,8 +167,7 @@ test('Lock a post', async () => { let postRes = await createPost(alpha, betaCommunity.community.id); // Lock the post - let searchBeta = await searchPost(beta, postRes.post_view.post); - let betaPost1 = searchBeta.posts[0]; + let betaPost1 = (await resolvePost(beta, postRes.post_view.post)).post; let lockedPostRes = await lockPost(beta, true, betaPost1.post); expect(lockedPostRes.post_view.post.locked).toBe(true); @@ -213,8 +205,7 @@ test('Delete a post', async () => { expect(deletedPost.post_view.post.name).toBe(""); // Make sure lemmy beta sees post is deleted - let searchBeta = await searchPost(beta, postRes.post_view.post); - let betaPost = searchBeta.posts[0]; + let betaPost = (await resolvePost(beta, postRes.post_view.post)).post; // This will be undefined because of the tombstone expect(betaPost).toBeUndefined(); @@ -223,8 +214,7 @@ test('Delete a post', async () => { expect(undeletedPost.post_view.post.deleted).toBe(false); // Make sure lemmy beta sees post is undeleted - let searchBeta2 = await searchPost(beta, postRes.post_view.post); - let betaPost2 = searchBeta2.posts[0]; + let betaPost2 = (await resolvePost(beta, postRes.post_view.post)).post; expect(betaPost2.post.deleted).toBe(false); assertPostFederation(betaPost2, undeletedPost.post_view); @@ -241,8 +231,7 @@ test('Remove a post from admin and community on different instance', async () => expect(removedPost.post_view.post.name).toBe(""); // Make sure lemmy beta sees post is NOT removed - let searchBeta = await searchPost(beta, postRes.post_view.post); - let betaPost = searchBeta.posts[0]; + let betaPost = (await resolvePost(beta, postRes.post_view.post)).post; expect(betaPost.post.removed).toBe(false); // Undelete @@ -250,8 +239,7 @@ test('Remove a post from admin and community on different instance', async () => expect(undeletedPost.post_view.post.removed).toBe(false); // Make sure lemmy beta sees post is undeleted - let searchBeta2 = await searchPost(beta, postRes.post_view.post); - let betaPost2 = searchBeta2.posts[0]; + let betaPost2 = (await resolvePost(beta, postRes.post_view.post)).post; expect(betaPost2.post.removed).toBe(false); assertPostFederation(betaPost2, undeletedPost.post_view); }); @@ -291,27 +279,26 @@ test('Search for a post', async () => { let postRes = await createPost(alpha, betaCommunity.community.id); expect(postRes.post_view.post).toBeDefined(); - let searchBeta = await searchPost(beta, postRes.post_view.post); + let betaPost = (await resolvePost(beta, postRes.post_view.post)).post; - expect(searchBeta.posts[0].post.name).toBeDefined(); + expect(betaPost.post.name).toBeDefined(); }); test('A and G subscribe to B (center) A posts, it gets announced to G', async () => { let postRes = await createPost(alpha, betaCommunity.community.id); expect(postRes.post_view.post).toBeDefined(); - let search2 = await searchPost(gamma, postRes.post_view.post); - expect(search2.posts[0].post.name).toBeDefined(); + let betaPost = (await resolvePost(gamma, postRes.post_view.post)).post; + expect(betaPost.post.name).toBeDefined(); }); test('Enforce site ban for federated user', async () => { let alphaShortname = `@lemmy_alpha@lemmy-alpha:8541`; - let userSearch = await searchForUser(beta, alphaShortname); - let alphaUser = userSearch.users[0]; - expect(alphaUser).toBeDefined(); + let alphaPerson = (await resolvePerson(beta, alphaShortname)).person; + expect(alphaPerson).toBeDefined(); // ban alpha from beta site - let banAlpha = await banPersonFromSite(beta, alphaUser.person.id, true); + let banAlpha = await banPersonFromSite(beta, alphaPerson.person.id, true); expect(banAlpha.banned).toBe(true); // Alpha makes post on beta @@ -327,19 +314,18 @@ test('Enforce site ban for federated user', async () => { expect(betaPost).toBeUndefined(); // Unban alpha - let unBanAlpha = await banPersonFromSite(beta, alphaUser.person.id, false); + let unBanAlpha = await banPersonFromSite(beta, alphaPerson.person.id, false); expect(unBanAlpha.banned).toBe(false); }); test('Enforce community ban for federated user', async () => { let alphaShortname = `@lemmy_alpha@lemmy-alpha:8541`; - let userSearch = await searchForUser(beta, alphaShortname); - let alphaUser = userSearch.users[0]; - expect(alphaUser).toBeDefined(); + let alphaPerson = (await resolvePerson(beta, alphaShortname)).person; + expect(alphaPerson).toBeDefined(); // ban alpha from beta site - await banPersonFromCommunity(beta, alphaUser.person.id, 2, false); - let banAlpha = await banPersonFromCommunity(beta, alphaUser.person.id, 2, true); + await banPersonFromCommunity(beta, alphaPerson.person.id, 2, false); + let banAlpha = await banPersonFromCommunity(beta, alphaPerson.person.id, 2, true); expect(banAlpha.banned).toBe(true); // Alpha tries to make post on beta, but it fails because of ban @@ -349,7 +335,7 @@ test('Enforce community ban for federated user', async () => { // Unban alpha let unBanAlpha = await banPersonFromCommunity( beta, - alphaUser.person.id, + alphaPerson.person.id, 2, false ); diff --git a/api_tests/src/shared.ts b/api_tests/src/shared.ts index 2de66c05b..1a1f9a5c3 100644 --- a/api_tests/src/shared.ts +++ b/api_tests/src/shared.ts @@ -47,6 +47,8 @@ import { BanFromCommunityResponse, Post, CreatePrivateMessage, + ResolveObjectResponse, + ResolveObject, } from 'lemmy-js-client'; export interface API { @@ -201,16 +203,14 @@ export async function lockPost( return api.client.lockPost(form); } -export async function searchPost( +export async function resolvePost( api: API, post: Post -): Promise { - let form: Search = { +): Promise { + let form: ResolveObject = { q: post.ap_id, - type_: SearchType.Posts, - sort: SortType.TopAll, }; - return api.client.search(form); + return api.client.resolveObject(form); } export async function searchPostLocal( @@ -235,56 +235,44 @@ export async function getPost( return api.client.getPost(form); } -export async function searchComment( +export async function resolveComment( api: API, comment: Comment -): Promise { - let form: Search = { +): Promise { + let form: ResolveObject = { q: comment.ap_id, - type_: SearchType.Comments, - sort: SortType.TopAll, }; - return api.client.search(form); + return api.client.resolveObject(form); } -export async function searchForBetaCommunity( +export async function resolveBetaCommunity( api: API -): Promise { - // Make sure lemmy-beta/c/main is cached on lemmy_alpha +): Promise { // Use short-hand search url - let form: Search = { + let form: ResolveObject = { q: '!main@lemmy-beta:8551', - type_: SearchType.Communities, - sort: SortType.TopAll, }; - return api.client.search(form); + return api.client.resolveObject(form); } -export async function searchForCommunity( +export async function resolveCommunity( api: API, q: string -): Promise { - // Use short-hand search url - let form: Search = { +): Promise { + let form: ResolveObject = { q, - type_: SearchType.Communities, - sort: SortType.TopAll, }; - return api.client.search(form); + return api.client.resolveObject(form); } -export async function searchForUser( +export async function resolvePerson( api: API, apShortname: string -): Promise { - // Make sure lemmy-beta/c/main is cached on lemmy_alpha - // Use short-hand search url - let form: Search = { +): Promise { + let form: ResolveObject = { q: apShortname, - type_: SearchType.Users, - sort: SortType.TopAll, }; - return api.client.search(form); + return api.client.resolveObject(form); } export async function banPersonFromSite( @@ -293,7 +281,6 @@ export async function banPersonFromSite( ban: boolean ): Promise { // Make sure lemmy-beta/c/main is cached on lemmy_alpha - // Use short-hand search url let form: BanPerson = { person_id, ban, @@ -310,7 +297,6 @@ export async function banPersonFromCommunity( ban: boolean ): Promise { // Make sure lemmy-beta/c/main is cached on lemmy_alpha - // Use short-hand search url let form: BanFromCommunity = { person_id, community_id, @@ -591,11 +577,9 @@ export async function unfollowRemotes( } export async function followBeta(api: API): Promise { - // Cache it - let search = await searchForBetaCommunity(api); - let com = search.communities.find(c => c.community.local == false); - if (com) { - let follow = await followCommunity(api, true, com.community.id); + let betaCommunity = (await resolveBetaCommunity(api)).community; + if (betaCommunity) { + let follow = await followCommunity(api, true, betaCommunity.community.id); return follow; } } diff --git a/api_tests/src/user.spec.ts b/api_tests/src/user.spec.ts index acbe8fe15..788987e2c 100644 --- a/api_tests/src/user.spec.ts +++ b/api_tests/src/user.spec.ts @@ -3,7 +3,7 @@ import { alpha, beta, registerUser, - searchForUser, + resolvePerson, saveUserSettings, getSite, } from './shared'; @@ -56,9 +56,7 @@ test('Set some user settings, check that they are federated', async () => { }; await saveUserSettings(alpha, form); - let searchAlpha = await searchForUser(alpha, apShortname); - let userOnAlpha = searchAlpha.users[0]; - let searchBeta = await searchForUser(beta, apShortname); - let userOnBeta = searchBeta.users[0]; - assertUserFederation(userOnAlpha, userOnBeta); + let alphaPerson = (await resolvePerson(alpha, apShortname)).person; + let betaPerson = (await resolvePerson(beta, apShortname)).person; + assertUserFederation(alphaPerson, betaPerson); }); diff --git a/api_tests/yarn.lock b/api_tests/yarn.lock index 370873b1e..e148c0fd2 100644 --- a/api_tests/yarn.lock +++ b/api_tests/yarn.lock @@ -3076,10 +3076,10 @@ language-tags@^1.0.5: dependencies: language-subtag-registry "~0.3.2" -lemmy-js-client@0.11.4-rc.9: - version "0.11.4-rc.9" - resolved "https://registry.yarnpkg.com/lemmy-js-client/-/lemmy-js-client-0.11.4-rc.9.tgz#f7b3c73691e4c1600daf3840d22d9cfbddc5f363" - integrity sha512-zP8JxWzQU+yuyE8cMG0GzR8aR3lJ++G5zzbynsXwDevzAZXhOm0ObNNtJiA3Q5msStFVKVYa3GwZxBv4XiYshw== +lemmy-js-client@0.11.4-rc.14: + version "0.11.4-rc.14" + resolved "https://registry.yarnpkg.com/lemmy-js-client/-/lemmy-js-client-0.11.4-rc.14.tgz#dcac5b8dc78c3b04e6b3630ff9351a94aa73e109" + integrity sha512-R8M+myyriNQljQlTweVqtUKGBpgmaM7RI4ebYb7N7sYr5Bk5Ip6v2qTNvKAV6BlsDOCTWANOonfeoz/cIerLEg== leven@^3.1.0: version "3.1.0" diff --git a/crates/api/src/site.rs b/crates/api/src/site.rs index 315cf72da..b80467e37 100644 --- a/crates/api/src/site.rs +++ b/crates/api/src/site.rs @@ -376,7 +376,10 @@ impl Perform for ResolveObject { _websocket_id: Option, ) -> Result { let local_user_view = get_local_user_view_from_jwt_opt(&self.auth, context.pool()).await?; - search_by_apub_id(&self.q, local_user_view, context).await + let res = search_by_apub_id(&self.q, local_user_view, context) + .await + .map_err(|_| ApiError::err("couldnt_find_object"))?; + Ok(res) } } diff --git a/crates/api_common/src/site.rs b/crates/api_common/src/site.rs index ed6211781..03f86f3dc 100644 --- a/crates/api_common/src/site.rs +++ b/crates/api_common/src/site.rs @@ -56,12 +56,12 @@ pub struct ResolveObject { pub auth: Option, } -#[derive(Serialize, Debug)] -pub enum ResolveObjectResponse { - Comment(CommentView), - Post(PostView), - Community(CommunityView), - Person(PersonViewSafe), +#[derive(Serialize, Default)] +pub struct ResolveObjectResponse { + pub comment: Option, + pub post: Option, + pub community: Option, + pub person: Option, } #[derive(Deserialize)] diff --git a/crates/apub/src/fetcher/search.rs b/crates/apub/src/fetcher/search.rs index 6f081c8b4..70e7c40c1 100644 --- a/crates/apub/src/fetcher/search.rs +++ b/crates/apub/src/fetcher/search.rs @@ -83,7 +83,10 @@ pub async fn search_by_apub_id( CommunityView::read(conn, community.id, local_user_view.map(|l| l.person.id)) }) .await??; - Ok(ResolveObjectResponse::Community(res)) + Ok(ResolveObjectResponse { + community: Some(res), + ..ResolveObjectResponse::default() + }) } WebfingerType::Person => { let res = blocking(context.pool(), move |conn| { @@ -91,7 +94,10 @@ pub async fn search_by_apub_id( PersonViewSafe::read(conn, person.id) }) .await??; - Ok(ResolveObjectResponse::Person(res)) + Ok(ResolveObjectResponse { + person: Some(res), + ..ResolveObjectResponse::default() + }) } }; } @@ -126,36 +132,47 @@ async fn build_response( let person_uri = p.id(&query_url)?; let person = get_or_fetch_and_upsert_person(person_uri, context, recursion_counter).await?; - ROR::Person( - blocking(context.pool(), move |conn| { + ROR { + person: blocking(context.pool(), move |conn| { PersonViewSafe::read(conn, person.id) }) - .await??, - ) + .await? + .ok(), + ..ROR::default() + } } SearchAcceptedObjects::Group(g) => { let community_uri = g.id(&query_url)?; let community = get_or_fetch_and_upsert_community(community_uri, context, recursion_counter).await?; - ROR::Community( - blocking(context.pool(), move |conn| { + ROR { + community: blocking(context.pool(), move |conn| { CommunityView::read(conn, community.id, None) }) - .await??, - ) + .await? + .ok(), + ..ROR::default() + } } SearchAcceptedObjects::Page(p) => { let p = Post::from_apub(&p, context, &query_url, recursion_counter).await?; - ROR::Post(blocking(context.pool(), move |conn| PostView::read(conn, p.id, None)).await??) + ROR { + post: blocking(context.pool(), move |conn| PostView::read(conn, p.id, None)) + .await? + .ok(), + ..ROR::default() + } } SearchAcceptedObjects::Comment(c) => { let c = Comment::from_apub(&c, context, &query_url, recursion_counter).await?; - ROR::Comment( - blocking(context.pool(), move |conn| { + ROR { + comment: blocking(context.pool(), move |conn| { CommentView::read(conn, c.id, None) }) - .await??, - ) + .await? + .ok(), + ..ROR::default() + } } }) }