mirror of https://github.com/LemmyNet/lemmy.git
rewrite apub object search to be generic
parent
64619d7eb1
commit
d4330dfea5
|
@ -135,7 +135,8 @@ test('Update a post', async () => {
|
||||||
test('Sticky a post', async () => {
|
test('Sticky a post', async () => {
|
||||||
let postRes = await createPost(alpha, betaCommunity.community.id);
|
let postRes = await createPost(alpha, betaCommunity.community.id);
|
||||||
|
|
||||||
let stickiedPostRes = await stickyPost(alpha, true, postRes.post_view.post);
|
let betaPost1 = (await resolvePost(beta, postRes.post_view.post)).post;
|
||||||
|
let stickiedPostRes = await stickyPost(beta, true, betaPost1.post);
|
||||||
expect(stickiedPostRes.post_view.post.stickied).toBe(true);
|
expect(stickiedPostRes.post_view.post.stickied).toBe(true);
|
||||||
|
|
||||||
// Make sure that post is stickied on beta
|
// Make sure that post is stickied on beta
|
||||||
|
@ -145,7 +146,7 @@ test('Sticky a post', async () => {
|
||||||
expect(betaPost.post.stickied).toBe(true);
|
expect(betaPost.post.stickied).toBe(true);
|
||||||
|
|
||||||
// Unsticky a post
|
// Unsticky a post
|
||||||
let unstickiedPost = await stickyPost(alpha, false, postRes.post_view.post);
|
let unstickiedPost = await stickyPost(beta, false, betaPost1.post);
|
||||||
expect(unstickiedPost.post_view.post.stickied).toBe(false);
|
expect(unstickiedPost.post_view.post.stickied).toBe(false);
|
||||||
|
|
||||||
// Make sure that post is unstickied on beta
|
// Make sure that post is unstickied on beta
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use crate::Perform;
|
use crate::Perform;
|
||||||
use actix_web::web::Data;
|
use actix_web::web::Data;
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
|
use diesel::NotFound;
|
||||||
use lemmy_api_common::{
|
use lemmy_api_common::{
|
||||||
blocking,
|
blocking,
|
||||||
build_federated_instances,
|
build_federated_instances,
|
||||||
|
@ -9,24 +10,32 @@ use lemmy_api_common::{
|
||||||
is_admin,
|
is_admin,
|
||||||
site::*,
|
site::*,
|
||||||
};
|
};
|
||||||
use lemmy_apub::{build_actor_id_from_shortname, fetcher::search::search_by_apub_id, EndpointType};
|
use lemmy_apub::{
|
||||||
|
build_actor_id_from_shortname,
|
||||||
|
fetcher::search::{search_by_apub_id, SearchableObjects},
|
||||||
|
EndpointType,
|
||||||
|
};
|
||||||
use lemmy_db_queries::{
|
use lemmy_db_queries::{
|
||||||
from_opt_str_to_opt_enum,
|
from_opt_str_to_opt_enum,
|
||||||
source::site::Site_,
|
source::site::Site_,
|
||||||
Crud,
|
Crud,
|
||||||
|
DbPool,
|
||||||
DeleteableOrRemoveable,
|
DeleteableOrRemoveable,
|
||||||
ListingType,
|
ListingType,
|
||||||
SearchType,
|
SearchType,
|
||||||
SortType,
|
SortType,
|
||||||
};
|
};
|
||||||
use lemmy_db_schema::source::{moderator::*, site::Site};
|
use lemmy_db_schema::{
|
||||||
|
source::{moderator::*, site::Site},
|
||||||
|
PersonId,
|
||||||
|
};
|
||||||
use lemmy_db_views::{
|
use lemmy_db_views::{
|
||||||
comment_view::CommentQueryBuilder,
|
comment_view::{CommentQueryBuilder, CommentView},
|
||||||
post_view::PostQueryBuilder,
|
post_view::{PostQueryBuilder, PostView},
|
||||||
site_view::SiteView,
|
site_view::SiteView,
|
||||||
};
|
};
|
||||||
use lemmy_db_views_actor::{
|
use lemmy_db_views_actor::{
|
||||||
community_view::CommunityQueryBuilder,
|
community_view::{CommunityQueryBuilder, CommunityView},
|
||||||
person_view::{PersonQueryBuilder, PersonViewSafe},
|
person_view::{PersonQueryBuilder, PersonViewSafe},
|
||||||
};
|
};
|
||||||
use lemmy_db_views_moderator::{
|
use lemmy_db_views_moderator::{
|
||||||
|
@ -376,13 +385,54 @@ impl Perform for ResolveObject {
|
||||||
_websocket_id: Option<ConnectionId>,
|
_websocket_id: Option<ConnectionId>,
|
||||||
) -> Result<ResolveObjectResponse, LemmyError> {
|
) -> Result<ResolveObjectResponse, LemmyError> {
|
||||||
let local_user_view = get_local_user_view_from_jwt_opt(&self.auth, context.pool()).await?;
|
let local_user_view = get_local_user_view_from_jwt_opt(&self.auth, context.pool()).await?;
|
||||||
let res = search_by_apub_id(&self.q, local_user_view, context)
|
let res = search_by_apub_id(&self.q, context)
|
||||||
.await
|
.await
|
||||||
.map_err(|_| ApiError::err("couldnt_find_object"))?;
|
.map_err(|_| ApiError::err("couldnt_find_object"))?;
|
||||||
Ok(res)
|
convert_response(res, local_user_view.map(|l| l.person.id), context.pool())
|
||||||
|
.await
|
||||||
|
.map_err(|_| ApiError::err("couldnt_find_object").into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn convert_response(
|
||||||
|
object: SearchableObjects,
|
||||||
|
user_id: Option<PersonId>,
|
||||||
|
pool: &DbPool,
|
||||||
|
) -> Result<ResolveObjectResponse, LemmyError> {
|
||||||
|
let removed_or_deleted;
|
||||||
|
let mut res = ResolveObjectResponse {
|
||||||
|
comment: None,
|
||||||
|
post: None,
|
||||||
|
community: None,
|
||||||
|
person: None,
|
||||||
|
};
|
||||||
|
use SearchableObjects::*;
|
||||||
|
match object {
|
||||||
|
Person(p) => {
|
||||||
|
removed_or_deleted = p.deleted;
|
||||||
|
res.person = Some(blocking(pool, move |conn| PersonViewSafe::read(conn, p.id)).await??)
|
||||||
|
}
|
||||||
|
Community(c) => {
|
||||||
|
removed_or_deleted = c.deleted || c.removed;
|
||||||
|
res.community =
|
||||||
|
Some(blocking(pool, move |conn| CommunityView::read(conn, c.id, user_id)).await??)
|
||||||
|
}
|
||||||
|
Post(p) => {
|
||||||
|
removed_or_deleted = p.deleted || p.removed;
|
||||||
|
res.post = Some(blocking(pool, move |conn| PostView::read(conn, p.id, user_id)).await??)
|
||||||
|
}
|
||||||
|
Comment(c) => {
|
||||||
|
removed_or_deleted = c.deleted || c.removed;
|
||||||
|
res.comment = Some(blocking(pool, move |conn| CommentView::read(conn, c.id, user_id)).await??)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// if the object was deleted from database, dont return it
|
||||||
|
if removed_or_deleted {
|
||||||
|
return Err(NotFound {}.into());
|
||||||
|
}
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
|
||||||
#[async_trait::async_trait(?Send)]
|
#[async_trait::async_trait(?Send)]
|
||||||
impl Perform for TransferSite {
|
impl Perform for TransferSite {
|
||||||
type Response = GetSiteResponse;
|
type Response = GetSiteResponse;
|
||||||
|
|
|
@ -49,6 +49,7 @@ use lemmy_websocket::{
|
||||||
UserOperationCrud,
|
UserOperationCrud,
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_with::skip_serializing_none;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
/// This is very confusing, because there are four distinct cases to handle:
|
/// This is very confusing, because there are four distinct cases to handle:
|
||||||
|
@ -59,6 +60,7 @@ use url::Url;
|
||||||
///
|
///
|
||||||
/// TODO: we should probably change how community deletions work to simplify this. Probably by
|
/// TODO: we should probably change how community deletions work to simplify this. Probably by
|
||||||
/// wrapping it in an announce just like other activities, instead of having the community send it.
|
/// wrapping it in an announce just like other activities, instead of having the community send it.
|
||||||
|
#[skip_serializing_none]
|
||||||
#[derive(Clone, Debug, Deserialize, Serialize, ActivityFields)]
|
#[derive(Clone, Debug, Deserialize, Serialize, ActivityFields)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct Delete {
|
pub struct Delete {
|
||||||
|
|
|
@ -2,10 +2,9 @@ use crate::{check_is_apub_id_valid, APUB_JSON_CONTENT_TYPE};
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
use lemmy_utils::{request::retry, LemmyError};
|
use lemmy_utils::{request::retry, LemmyError};
|
||||||
use log::info;
|
use log::info;
|
||||||
use reqwest::{Client, StatusCode};
|
use reqwest::Client;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use thiserror::Error;
|
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
/// Maximum number of HTTP requests allowed to handle a single incoming activity (or a single object
|
/// Maximum number of HTTP requests allowed to handle a single incoming activity (or a single object
|
||||||
|
@ -15,50 +14,19 @@ use url::Url;
|
||||||
/// So we are looking at a maximum of 22 requests (rounded up just to be safe).
|
/// So we are looking at a maximum of 22 requests (rounded up just to be safe).
|
||||||
static MAX_REQUEST_NUMBER: i32 = 25;
|
static MAX_REQUEST_NUMBER: i32 = 25;
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
|
||||||
pub(in crate::fetcher) struct FetchError {
|
|
||||||
pub inner: anyhow::Error,
|
|
||||||
pub status_code: Option<StatusCode>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<LemmyError> for FetchError {
|
|
||||||
fn from(t: LemmyError) -> Self {
|
|
||||||
FetchError {
|
|
||||||
inner: t.inner,
|
|
||||||
status_code: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<reqwest::Error> for FetchError {
|
|
||||||
fn from(t: reqwest::Error) -> Self {
|
|
||||||
let status = t.status();
|
|
||||||
FetchError {
|
|
||||||
inner: t.into(),
|
|
||||||
status_code: status,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for FetchError {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
|
||||||
std::fmt::Display::fmt(&self, f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fetch any type of ActivityPub object, handling things like HTTP headers, deserialisation,
|
/// Fetch any type of ActivityPub object, handling things like HTTP headers, deserialisation,
|
||||||
/// timeouts etc.
|
/// timeouts etc.
|
||||||
pub(in crate::fetcher) async fn fetch_remote_object<Response>(
|
pub(in crate::fetcher) async fn fetch_remote_object<Response>(
|
||||||
client: &Client,
|
client: &Client,
|
||||||
url: &Url,
|
url: &Url,
|
||||||
recursion_counter: &mut i32,
|
recursion_counter: &mut i32,
|
||||||
) -> Result<Response, FetchError>
|
) -> Result<Response, LemmyError>
|
||||||
where
|
where
|
||||||
Response: for<'de> Deserialize<'de> + std::fmt::Debug,
|
Response: for<'de> Deserialize<'de> + std::fmt::Debug,
|
||||||
{
|
{
|
||||||
*recursion_counter += 1;
|
*recursion_counter += 1;
|
||||||
if *recursion_counter > MAX_REQUEST_NUMBER {
|
if *recursion_counter > MAX_REQUEST_NUMBER {
|
||||||
return Err(LemmyError::from(anyhow!("Maximum recursion depth reached")).into());
|
return Err(anyhow!("Maximum recursion depth reached").into());
|
||||||
}
|
}
|
||||||
check_is_apub_id_valid(url, false)?;
|
check_is_apub_id_valid(url, false)?;
|
||||||
|
|
||||||
|
@ -73,14 +41,6 @@ where
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
if res.status() == StatusCode::GONE {
|
|
||||||
info!("Fetched remote object {} which was deleted", url);
|
|
||||||
return Err(FetchError {
|
|
||||||
inner: anyhow!("Fetched remote object {} which was deleted", url),
|
|
||||||
status_code: Some(res.status()),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let object = res.json().await?;
|
let object = res.json().await?;
|
||||||
info!("Fetched remote object {}", url);
|
info!("Fetched remote object {}", url);
|
||||||
Ok(object)
|
Ok(object)
|
||||||
|
|
|
@ -5,38 +5,19 @@ pub mod object_id;
|
||||||
pub mod post_or_comment;
|
pub mod post_or_comment;
|
||||||
pub mod search;
|
pub mod search;
|
||||||
|
|
||||||
use crate::{
|
use crate::{fetcher::object_id::ObjectId, ActorType};
|
||||||
fetcher::{fetch::FetchError, object_id::ObjectId},
|
|
||||||
ActorType,
|
|
||||||
};
|
|
||||||
use chrono::NaiveDateTime;
|
use chrono::NaiveDateTime;
|
||||||
use http::StatusCode;
|
|
||||||
use lemmy_db_schema::{
|
use lemmy_db_schema::{
|
||||||
naive_now,
|
naive_now,
|
||||||
source::{community::Community, person::Person},
|
source::{community::Community, person::Person},
|
||||||
};
|
};
|
||||||
use lemmy_utils::LemmyError;
|
use lemmy_utils::LemmyError;
|
||||||
use lemmy_websocket::LemmyContext;
|
use lemmy_websocket::LemmyContext;
|
||||||
use serde::Deserialize;
|
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
static ACTOR_REFETCH_INTERVAL_SECONDS: i64 = 24 * 60 * 60;
|
static ACTOR_REFETCH_INTERVAL_SECONDS: i64 = 24 * 60 * 60;
|
||||||
static ACTOR_REFETCH_INTERVAL_SECONDS_DEBUG: i64 = 10;
|
static ACTOR_REFETCH_INTERVAL_SECONDS_DEBUG: i64 = 10;
|
||||||
|
|
||||||
fn is_deleted<Response>(fetch_response: &Result<Response, FetchError>) -> bool
|
|
||||||
where
|
|
||||||
Response: for<'de> Deserialize<'de>,
|
|
||||||
{
|
|
||||||
if let Err(e) = fetch_response {
|
|
||||||
if let Some(status) = e.status_code {
|
|
||||||
if status == StatusCode::GONE {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get a remote actor from its apub ID (either a person or a community). Thin wrapper around
|
/// Get a remote actor from its apub ID (either a person or a community). Thin wrapper around
|
||||||
/// `get_or_fetch_and_upsert_person()` and `get_or_fetch_and_upsert_community()`.
|
/// `get_or_fetch_and_upsert_person()` and `get_or_fetch_and_upsert_community()`.
|
||||||
///
|
///
|
||||||
|
|
|
@ -14,7 +14,7 @@ use log::debug;
|
||||||
use reqwest::StatusCode;
|
use reqwest::StatusCode;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{
|
use std::{
|
||||||
fmt::{Display, Formatter},
|
fmt::{Debug, Display, Formatter},
|
||||||
marker::PhantomData,
|
marker::PhantomData,
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
|
@ -54,7 +54,9 @@ where
|
||||||
context: &LemmyContext,
|
context: &LemmyContext,
|
||||||
request_counter: &mut i32,
|
request_counter: &mut i32,
|
||||||
) -> Result<Kind, LemmyError> {
|
) -> Result<Kind, LemmyError> {
|
||||||
|
debug!("dereference {}", self.to_string());
|
||||||
let db_object = self.dereference_locally(context.pool()).await?;
|
let db_object = self.dereference_locally(context.pool()).await?;
|
||||||
|
|
||||||
// if its a local object, only fetch it from the database and not over http
|
// if its a local object, only fetch it from the database and not over http
|
||||||
if self.0.domain() == Some(&Settings::get().get_hostname_without_port()?) {
|
if self.0.domain() == Some(&Settings::get().get_hostname_without_port()?) {
|
||||||
return match db_object {
|
return match db_object {
|
||||||
|
@ -84,6 +86,7 @@ where
|
||||||
|
|
||||||
/// returning none means the object was not found in local db
|
/// returning none means the object was not found in local db
|
||||||
async fn dereference_locally(&self, pool: &DbPool) -> Result<Option<Kind>, LemmyError> {
|
async fn dereference_locally(&self, pool: &DbPool) -> Result<Option<Kind>, LemmyError> {
|
||||||
|
debug!("dereference_locally {}", self.to_string());
|
||||||
let id: DbUrl = self.0.clone().into();
|
let id: DbUrl = self.0.clone().into();
|
||||||
let object = blocking(pool, move |conn| ApubObject::read_from_apub_id(conn, &id)).await?;
|
let object = blocking(pool, move |conn| ApubObject::read_from_apub_id(conn, &id)).await?;
|
||||||
match object {
|
match object {
|
||||||
|
@ -99,6 +102,7 @@ where
|
||||||
request_counter: &mut i32,
|
request_counter: &mut i32,
|
||||||
db_object: Option<Kind>,
|
db_object: Option<Kind>,
|
||||||
) -> Result<Kind, LemmyError> {
|
) -> Result<Kind, LemmyError> {
|
||||||
|
debug!("dereference_remotely {}", self.to_string());
|
||||||
// dont fetch local objects this way
|
// dont fetch local objects this way
|
||||||
debug_assert!(self.0.domain() != Some(&Settings::get().hostname));
|
debug_assert!(self.0.domain() != Some(&Settings::get().hostname));
|
||||||
|
|
||||||
|
@ -118,6 +122,7 @@ where
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
if res.status() == StatusCode::GONE {
|
if res.status() == StatusCode::GONE {
|
||||||
|
debug!("is deleted {}", self.to_string());
|
||||||
if let Some(db_object) = db_object {
|
if let Some(db_object) = db_object {
|
||||||
db_object.delete(context).await?;
|
db_object.delete(context).await?;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,47 +1,27 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
fetcher::{fetch::fetch_remote_object, is_deleted, object_id::ObjectId},
|
fetcher::{deletable_apub_object::DeletableApubObject, object_id::ObjectId},
|
||||||
find_object_by_id,
|
|
||||||
objects::{comment::Note, community::Group, person::Person as ApubPerson, post::Page, FromApub},
|
objects::{comment::Note, community::Group, person::Person as ApubPerson, post::Page, FromApub},
|
||||||
Object,
|
|
||||||
};
|
};
|
||||||
|
use activitystreams::chrono::NaiveDateTime;
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
|
use diesel::{result::Error, PgConnection};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use lemmy_api_common::{blocking, site::ResolveObjectResponse};
|
use lemmy_api_common::blocking;
|
||||||
use lemmy_apub_lib::webfinger::{webfinger_resolve_actor, WebfingerType};
|
use lemmy_apub_lib::webfinger::{webfinger_resolve_actor, WebfingerType};
|
||||||
use lemmy_db_queries::source::{
|
use lemmy_db_queries::{
|
||||||
comment::Comment_,
|
source::{community::Community_, person::Person_},
|
||||||
community::Community_,
|
ApubObject,
|
||||||
person::Person_,
|
DbPool,
|
||||||
post::Post_,
|
|
||||||
private_message::PrivateMessage_,
|
|
||||||
};
|
};
|
||||||
use lemmy_db_schema::source::{
|
use lemmy_db_schema::{
|
||||||
comment::Comment,
|
source::{comment::Comment, community::Community, person::Person, post::Post},
|
||||||
community::Community,
|
DbUrl,
|
||||||
person::Person,
|
|
||||||
post::Post,
|
|
||||||
private_message::PrivateMessage,
|
|
||||||
};
|
};
|
||||||
use lemmy_db_views::{
|
|
||||||
comment_view::CommentView,
|
|
||||||
local_user_view::LocalUserView,
|
|
||||||
post_view::PostView,
|
|
||||||
};
|
|
||||||
use lemmy_db_views_actor::{community_view::CommunityView, person_view::PersonViewSafe};
|
|
||||||
use lemmy_utils::LemmyError;
|
use lemmy_utils::LemmyError;
|
||||||
use lemmy_websocket::LemmyContext;
|
use lemmy_websocket::LemmyContext;
|
||||||
|
use serde::Deserialize;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
/// The types of ActivityPub objects that can be fetched directly by searching for their ID.
|
|
||||||
#[derive(serde::Deserialize, Debug)]
|
|
||||||
#[serde(untagged)]
|
|
||||||
enum SearchAcceptedObjects {
|
|
||||||
Person(Box<ApubPerson>),
|
|
||||||
Group(Box<Group>),
|
|
||||||
Page(Box<Page>),
|
|
||||||
Comment(Box<Note>),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Attempt to parse the query as URL, and fetch an ActivityPub object from it.
|
/// Attempt to parse the query as URL, and fetch an ActivityPub object from it.
|
||||||
///
|
///
|
||||||
/// Some working examples for use with the `docker/federation/` setup:
|
/// Some working examples for use with the `docker/federation/` setup:
|
||||||
|
@ -51,9 +31,8 @@ enum SearchAcceptedObjects {
|
||||||
/// http://lemmy_delta:8571/comment/2
|
/// http://lemmy_delta:8571/comment/2
|
||||||
pub async fn search_by_apub_id(
|
pub async fn search_by_apub_id(
|
||||||
query: &str,
|
query: &str,
|
||||||
local_user_view: Option<LocalUserView>,
|
|
||||||
context: &LemmyContext,
|
context: &LemmyContext,
|
||||||
) -> Result<ResolveObjectResponse, LemmyError> {
|
) -> Result<SearchableObjects, LemmyError> {
|
||||||
let query_url = match Url::parse(query) {
|
let query_url = match Url::parse(query) {
|
||||||
Ok(u) => u,
|
Ok(u) => u,
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
|
@ -70,144 +49,120 @@ pub async fn search_by_apub_id(
|
||||||
}
|
}
|
||||||
// local actor, read from database and return
|
// local actor, read from database and return
|
||||||
else {
|
else {
|
||||||
let name: String = name.into();
|
return find_local_actor_by_name(name, kind, context.pool()).await;
|
||||||
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: Some(res),
|
|
||||||
..ResolveObjectResponse::default()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
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: Some(res),
|
|
||||||
..ResolveObjectResponse::default()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let request_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
|
ObjectId::<SearchableObjects>::new(query_url)
|
||||||
// below. we need to fix this when rewriting the fetcher.
|
.dereference(context, request_counter)
|
||||||
let fetch_response =
|
.await
|
||||||
fetch_remote_object::<SearchAcceptedObjects>(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, request_counter, context).await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn build_response(
|
async fn find_local_actor_by_name(
|
||||||
fetch_response: SearchAcceptedObjects,
|
name: &str,
|
||||||
query_url: Url,
|
kind: WebfingerType,
|
||||||
recursion_counter: &mut i32,
|
pool: &DbPool,
|
||||||
|
) -> Result<SearchableObjects, LemmyError> {
|
||||||
|
let name: String = name.into();
|
||||||
|
Ok(match kind {
|
||||||
|
WebfingerType::Group => SearchableObjects::Community(
|
||||||
|
blocking(pool, move |conn| Community::read_from_name(conn, &name)).await??,
|
||||||
|
),
|
||||||
|
WebfingerType::Person => SearchableObjects::Person(
|
||||||
|
blocking(pool, move |conn| Person::find_by_name(conn, &name)).await??,
|
||||||
|
),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The types of ActivityPub objects that can be fetched directly by searching for their ID.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum SearchableObjects {
|
||||||
|
Person(Person),
|
||||||
|
Community(Community),
|
||||||
|
Post(Post),
|
||||||
|
Comment(Comment),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum SearchableApubTypes {
|
||||||
|
Group(Group),
|
||||||
|
Person(ApubPerson),
|
||||||
|
Page(Page),
|
||||||
|
Note(Note),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ApubObject for SearchableObjects {
|
||||||
|
type Form = ();
|
||||||
|
|
||||||
|
fn last_refreshed_at(&self) -> Option<NaiveDateTime> {
|
||||||
|
match self {
|
||||||
|
SearchableObjects::Person(p) => p.last_refreshed_at(),
|
||||||
|
SearchableObjects::Community(c) => c.last_refreshed_at(),
|
||||||
|
SearchableObjects::Post(p) => p.last_refreshed_at(),
|
||||||
|
SearchableObjects::Comment(c) => c.last_refreshed_at(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: this is inefficient, because if the object is not in local db, it will run 4 db queries
|
||||||
|
// before finally returning an error. it would be nice if we could check all 4 tables in
|
||||||
|
// a single query.
|
||||||
|
// we could skip this and always return an error, but then it would not be able to mark
|
||||||
|
// objects as deleted that were deleted by remote server.
|
||||||
|
fn read_from_apub_id(conn: &PgConnection, object_id: &DbUrl) -> Result<Self, Error> {
|
||||||
|
let c = Community::read_from_apub_id(conn, object_id);
|
||||||
|
if let Ok(c) = c {
|
||||||
|
return Ok(SearchableObjects::Community(c));
|
||||||
|
}
|
||||||
|
let p = Person::read_from_apub_id(conn, object_id);
|
||||||
|
if let Ok(p) = p {
|
||||||
|
return Ok(SearchableObjects::Person(p));
|
||||||
|
}
|
||||||
|
let p = Post::read_from_apub_id(conn, object_id);
|
||||||
|
if let Ok(p) = p {
|
||||||
|
return Ok(SearchableObjects::Post(p));
|
||||||
|
}
|
||||||
|
let c = Comment::read_from_apub_id(conn, object_id);
|
||||||
|
Ok(SearchableObjects::Comment(c?))
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: move this (and the Form type) elsewhere, as it isnt really needed on this trait
|
||||||
|
fn upsert(_conn: &PgConnection, _user_form: &Self::Form) -> Result<Self, Error> {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait(?Send)]
|
||||||
|
impl FromApub for SearchableObjects {
|
||||||
|
type ApubType = SearchableApubTypes;
|
||||||
|
|
||||||
|
async fn from_apub(
|
||||||
|
apub: &Self::ApubType,
|
||||||
context: &LemmyContext,
|
context: &LemmyContext,
|
||||||
) -> Result<ResolveObjectResponse, LemmyError> {
|
ed: &Url,
|
||||||
use ResolveObjectResponse as ROR;
|
rc: &mut i32,
|
||||||
Ok(match fetch_response {
|
) -> Result<Self, LemmyError> {
|
||||||
SearchAcceptedObjects::Person(p) => {
|
use SearchableApubTypes as SAT;
|
||||||
let person_uri: ObjectId<Person> = ObjectId::<Person>::new(p.id(&query_url)?.clone());
|
use SearchableObjects as SO;
|
||||||
|
Ok(match apub {
|
||||||
let person = person_uri.dereference(context, recursion_counter).await?;
|
SAT::Group(g) => SO::Community(Community::from_apub(g, context, ed, rc).await?),
|
||||||
ROR {
|
SAT::Person(p) => SO::Person(Person::from_apub(p, context, ed, rc).await?),
|
||||||
person: blocking(context.pool(), move |conn| {
|
SAT::Page(p) => SO::Post(Post::from_apub(p, context, ed, rc).await?),
|
||||||
PersonViewSafe::read(conn, person.id)
|
SAT::Note(n) => SO::Comment(Comment::from_apub(n, context, ed, rc).await?),
|
||||||
})
|
})
|
||||||
.await?
|
|
||||||
.ok(),
|
|
||||||
..ROR::default()
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
SearchAcceptedObjects::Group(g) => {
|
|
||||||
let community_uri: ObjectId<Community> =
|
|
||||||
ObjectId::<Community>::new(g.id(&query_url)?.clone());
|
|
||||||
let community = community_uri
|
|
||||||
.dereference(context, recursion_counter)
|
|
||||||
.await?;
|
|
||||||
ROR {
|
|
||||||
community: blocking(context.pool(), move |conn| {
|
|
||||||
CommunityView::read(conn, community.id, None)
|
|
||||||
})
|
|
||||||
.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?
|
|
||||||
.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| {
|
|
||||||
CommentView::read(conn, c.id, None)
|
|
||||||
})
|
|
||||||
.await?
|
|
||||||
.ok(),
|
|
||||||
..ROR::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn delete_object_locally(query_url: &Url, context: &LemmyContext) -> Result<(), LemmyError> {
|
#[async_trait::async_trait(?Send)]
|
||||||
let res = find_object_by_id(context, query_url.to_owned()).await?;
|
impl DeletableApubObject for SearchableObjects {
|
||||||
match res {
|
async fn delete(self, context: &LemmyContext) -> Result<(), LemmyError> {
|
||||||
Object::Comment(c) => {
|
match self {
|
||||||
blocking(context.pool(), move |conn| {
|
SearchableObjects::Person(p) => p.delete(context).await,
|
||||||
Comment::update_deleted(conn, c.id, true)
|
SearchableObjects::Community(c) => c.delete(context).await,
|
||||||
})
|
SearchableObjects::Post(p) => p.delete(context).await,
|
||||||
.await??;
|
SearchableObjects::Comment(c) => c.delete(context).await,
|
||||||
}
|
|
||||||
Object::Post(p) => {
|
|
||||||
blocking(context.pool(), move |conn| {
|
|
||||||
Post::update_deleted(conn, p.id, true)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
}
|
|
||||||
Object::Person(u) => {
|
|
||||||
// TODO: implement update_deleted() for user, move it to ApubObject trait
|
|
||||||
blocking(context.pool(), move |conn| {
|
|
||||||
Person::delete_account(conn, u.id)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
}
|
|
||||||
Object::Community(c) => {
|
|
||||||
blocking(context.pool(), move |conn| {
|
|
||||||
Community::update_deleted(conn, c.id, true)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
}
|
|
||||||
Object::PrivateMessage(pm) => {
|
|
||||||
blocking(context.pool(), move |conn| {
|
|
||||||
PrivateMessage::update_deleted(conn, pm.id, true)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,24 +11,15 @@ pub mod objects;
|
||||||
|
|
||||||
use crate::{extensions::signatures::PublicKey, fetcher::post_or_comment::PostOrComment};
|
use crate::{extensions::signatures::PublicKey, fetcher::post_or_comment::PostOrComment};
|
||||||
use anyhow::{anyhow, Context};
|
use anyhow::{anyhow, Context};
|
||||||
use diesel::NotFound;
|
|
||||||
use lemmy_api_common::blocking;
|
use lemmy_api_common::blocking;
|
||||||
use lemmy_db_queries::{source::activity::Activity_, ApubObject, DbPool};
|
use lemmy_db_queries::{source::activity::Activity_, DbPool};
|
||||||
use lemmy_db_schema::{
|
use lemmy_db_schema::{
|
||||||
source::{
|
source::{activity::Activity, person::Person},
|
||||||
activity::Activity,
|
|
||||||
comment::Comment,
|
|
||||||
community::Community,
|
|
||||||
person::{Person as DbPerson, Person},
|
|
||||||
post::Post,
|
|
||||||
private_message::PrivateMessage,
|
|
||||||
},
|
|
||||||
CommunityId,
|
CommunityId,
|
||||||
DbUrl,
|
DbUrl,
|
||||||
};
|
};
|
||||||
use lemmy_db_views_actor::community_person_ban_view::CommunityPersonBanView;
|
use lemmy_db_views_actor::community_person_ban_view::CommunityPersonBanView;
|
||||||
use lemmy_utils::{location_info, settings::structs::Settings, LemmyError};
|
use lemmy_utils::{location_info, settings::structs::Settings, LemmyError};
|
||||||
use lemmy_websocket::LemmyContext;
|
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use std::net::IpAddr;
|
use std::net::IpAddr;
|
||||||
use url::{ParseError, Url};
|
use url::{ParseError, Url};
|
||||||
|
@ -244,81 +235,6 @@ where
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Tries to find a post or comment in the local database, without any network requests.
|
|
||||||
/// This is used to handle deletions and removals, because in case we dont have the object, we can
|
|
||||||
/// simply ignore the activity.
|
|
||||||
pub(crate) async fn find_post_or_comment_by_id(
|
|
||||||
context: &LemmyContext,
|
|
||||||
apub_id: Url,
|
|
||||||
) -> Result<PostOrComment, LemmyError> {
|
|
||||||
let ap_id = apub_id.clone();
|
|
||||||
let post = blocking(context.pool(), move |conn| {
|
|
||||||
Post::read_from_apub_id(conn, &ap_id.into())
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
if let Ok(p) = post {
|
|
||||||
return Ok(PostOrComment::Post(Box::new(p)));
|
|
||||||
}
|
|
||||||
|
|
||||||
let ap_id = apub_id.clone();
|
|
||||||
let comment = blocking(context.pool(), move |conn| {
|
|
||||||
Comment::read_from_apub_id(conn, &ap_id.into())
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
if let Ok(c) = comment {
|
|
||||||
return Ok(PostOrComment::Comment(Box::new(c)));
|
|
||||||
}
|
|
||||||
|
|
||||||
Err(NotFound.into())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
enum Object {
|
|
||||||
Comment(Box<Comment>),
|
|
||||||
Post(Box<Post>),
|
|
||||||
Community(Box<Community>),
|
|
||||||
Person(Box<DbPerson>),
|
|
||||||
PrivateMessage(Box<PrivateMessage>),
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn find_object_by_id(context: &LemmyContext, apub_id: Url) -> Result<Object, LemmyError> {
|
|
||||||
let ap_id = apub_id.clone();
|
|
||||||
if let Ok(pc) = find_post_or_comment_by_id(context, ap_id.to_owned()).await {
|
|
||||||
return Ok(match pc {
|
|
||||||
PostOrComment::Post(p) => Object::Post(Box::new(*p)),
|
|
||||||
PostOrComment::Comment(c) => Object::Comment(Box::new(*c)),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let ap_id = apub_id.clone();
|
|
||||||
let person = blocking(context.pool(), move |conn| {
|
|
||||||
DbPerson::read_from_apub_id(conn, &ap_id.into())
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
if let Ok(u) = person {
|
|
||||||
return Ok(Object::Person(Box::new(u)));
|
|
||||||
}
|
|
||||||
|
|
||||||
let ap_id = apub_id.clone();
|
|
||||||
let community = blocking(context.pool(), move |conn| {
|
|
||||||
Community::read_from_apub_id(conn, &ap_id.into())
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
if let Ok(c) = community {
|
|
||||||
return Ok(Object::Community(Box::new(c)));
|
|
||||||
}
|
|
||||||
|
|
||||||
let private_message = blocking(context.pool(), move |conn| {
|
|
||||||
PrivateMessage::read_from_apub_id(conn, &apub_id.into())
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
if let Ok(pm) = private_message {
|
|
||||||
return Ok(Object::PrivateMessage(Box::new(pm)));
|
|
||||||
}
|
|
||||||
|
|
||||||
Err(NotFound.into())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn check_community_or_site_ban(
|
async fn check_community_or_site_ban(
|
||||||
person: &Person,
|
person: &Person,
|
||||||
community_id: CommunityId,
|
community_id: CommunityId,
|
||||||
|
|
Loading…
Reference in New Issue