diff --git a/Cargo.lock b/Cargo.lock index 11ca872b2..4d33d21fe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1847,6 +1847,7 @@ dependencies = [ "actix-web", "chrono", "diesel", + "itertools", "lemmy_db_schema", "lemmy_db_views", "lemmy_db_views_actor", diff --git a/crates/api/src/site.rs b/crates/api/src/site.rs index c77b177fa..20ceffe57 100644 --- a/crates/api/src/site.rs +++ b/crates/api/src/site.rs @@ -8,22 +8,17 @@ use lemmy_api_common::{ get_local_user_view_from_jwt, get_local_user_view_from_jwt_opt, is_admin, + resolve_actor_identifier, send_application_approved_email, site::*, }; -use lemmy_apub::{ - fetcher::{ - search::{search_by_apub_id, SearchableObjects}, - webfinger::webfinger_resolve, - }, - objects::community::ApubCommunity, - EndpointType, -}; +use lemmy_apub::fetcher::search::{search_by_apub_id, SearchableObjects}; use lemmy_db_schema::{ diesel_option_overwrite, from_opt_str_to_opt_enum, newtypes::PersonId, source::{ + community::Community, local_user::{LocalUser, LocalUserForm}, moderator::*, person::Person, @@ -195,9 +190,10 @@ impl Perform for Search { let search_type: SearchType = from_opt_str_to_opt_enum(&data.type_).unwrap_or(SearchType::All); let community_id = data.community_id; let community_actor_id = if let Some(name) = &data.community_name { - webfinger_resolve::(name, EndpointType::Community, context, &mut 0) + resolve_actor_identifier::(name, context.pool()) .await .ok() + .map(|c| c.actor_id) } else { None }; diff --git a/crates/api_common/Cargo.toml b/crates/api_common/Cargo.toml index 44e8aa251..368432937 100644 --- a/crates/api_common/Cargo.toml +++ b/crates/api_common/Cargo.toml @@ -25,3 +25,4 @@ chrono = { version = "0.4.19", features = ["serde"] } serde_json = { version = "1.0.72", features = ["preserve_order"] } tracing = "0.1.29" url = "2.2.2" +itertools = "0.10.3" diff --git a/crates/api_common/src/lib.rs b/crates/api_common/src/lib.rs index a83f0071b..2d1f1bf50 100644 --- a/crates/api_common/src/lib.rs +++ b/crates/api_common/src/lib.rs @@ -6,6 +6,7 @@ pub mod site; pub mod websocket; use crate::site::FederatedInstances; +use itertools::Itertools; use lemmy_db_schema::{ newtypes::{CommunityId, LocalUserId, PersonId, PostId}, source::{ @@ -18,7 +19,7 @@ use lemmy_db_schema::{ secret::Secret, site::Site, }, - traits::{Crud, Readable}, + traits::{ApubActor, Crud, Readable}, DbPool, }; use lemmy_db_views::local_user_view::{LocalUserSettingsView, LocalUserView}; @@ -521,3 +522,36 @@ pub async fn check_private_instance_and_federation_enabled( } Ok(()) } + +/// Resolve actor identifier (eg `!news@example.com`) from local database to avoid network requests. +/// This only works for local actors, and remote actors which were previously fetched (so it doesnt +/// trigger any new fetch). +#[tracing::instrument(skip_all)] +pub async fn resolve_actor_identifier( + identifier: &str, + pool: &DbPool, +) -> Result +where + Actor: ApubActor + Send + 'static, +{ + // remote actor + if identifier.contains('@') { + let (name, domain) = identifier + .splitn(2, '@') + .collect_tuple() + .expect("invalid query"); + let name = name.to_string(); + let domain = format!("{}://{}", Settings::get().get_protocol_string(), domain); + Ok( + blocking(pool, move |conn| { + Actor::read_from_name_and_domain(conn, &name, &domain) + }) + .await??, + ) + } + // local actor + else { + let identifier = identifier.to_string(); + Ok(blocking(pool, move |conn| Actor::read_from_name(conn, &identifier)).await??) + } +} diff --git a/crates/api_crud/src/comment/read.rs b/crates/api_crud/src/comment/read.rs index 459469917..56584fc37 100644 --- a/crates/api_crud/src/comment/read.rs +++ b/crates/api_crud/src/comment/read.rs @@ -5,14 +5,11 @@ use lemmy_api_common::{ check_private_instance, comment::*, get_local_user_view_from_jwt_opt, -}; -use lemmy_apub::{ - fetcher::webfinger::webfinger_resolve, - objects::community::ApubCommunity, - EndpointType, + resolve_actor_identifier, }; use lemmy_db_schema::{ from_opt_str_to_opt_enum, + source::community::Community, traits::DeleteableOrRemoveable, ListingType, SortType, @@ -82,9 +79,10 @@ impl PerformCrud for GetComments { let community_id = data.community_id; let community_actor_id = if let Some(name) = &data.community_name { - webfinger_resolve::(name, EndpointType::Community, context, &mut 0) + resolve_actor_identifier::(name, context.pool()) .await .ok() + .map(|c| c.actor_id) } else { None }; diff --git a/crates/api_crud/src/community/read.rs b/crates/api_crud/src/community/read.rs index 2ec4054b2..7e61bfa71 100644 --- a/crates/api_crud/src/community/read.rs +++ b/crates/api_crud/src/community/read.rs @@ -5,15 +5,11 @@ use lemmy_api_common::{ check_private_instance, community::*, get_local_user_view_from_jwt_opt, + resolve_actor_identifier, }; -use lemmy_apub::{ - fetcher::webfinger::webfinger_resolve, - objects::community::ApubCommunity, - EndpointType, -}; -use lemmy_apub_lib::object_id::ObjectId; use lemmy_db_schema::{ from_opt_str_to_opt_enum, + source::community::Community, traits::DeleteableOrRemoveable, ListingType, SortType, @@ -48,12 +44,7 @@ impl PerformCrud for GetCommunity { Some(id) => id, None => { let name = data.name.to_owned().unwrap_or_else(|| "main".to_string()); - let community_actor_id = - webfinger_resolve::(&name, EndpointType::Community, context, &mut 0) - .await?; - - ObjectId::::new(community_actor_id) - .dereference(context, context.client(), &mut 0) + resolve_actor_identifier::(&name, context.pool()) .await .map_err(LemmyError::from) .map_err(|e| e.with_message("couldnt_find_community"))? diff --git a/crates/api_crud/src/post/read.rs b/crates/api_crud/src/post/read.rs index 10ecefc38..8d049e70a 100644 --- a/crates/api_crud/src/post/read.rs +++ b/crates/api_crud/src/post/read.rs @@ -6,14 +6,11 @@ use lemmy_api_common::{ get_local_user_view_from_jwt_opt, mark_post_as_read, post::*, -}; -use lemmy_apub::{ - fetcher::webfinger::webfinger_resolve, - objects::community::ApubCommunity, - EndpointType, + resolve_actor_identifier, }; use lemmy_db_schema::{ from_opt_str_to_opt_enum, + source::community::Community, traits::DeleteableOrRemoveable, ListingType, SortType, @@ -157,9 +154,10 @@ impl PerformCrud for GetPosts { let limit = data.limit; let community_id = data.community_id; let community_actor_id = if let Some(name) = &data.community_name { - webfinger_resolve::(name, EndpointType::Community, context, &mut 0) + resolve_actor_identifier::(name, context.pool()) .await .ok() + .map(|c| c.actor_id) } else { None }; diff --git a/crates/api_crud/src/user/read.rs b/crates/api_crud/src/user/read.rs index efa058b18..9368fabdf 100644 --- a/crates/api_crud/src/user/read.rs +++ b/crates/api_crud/src/user/read.rs @@ -5,14 +5,9 @@ use lemmy_api_common::{ check_private_instance, get_local_user_view_from_jwt_opt, person::*, + resolve_actor_identifier, }; -use lemmy_apub::{ - fetcher::webfinger::webfinger_resolve, - objects::person::ApubPerson, - EndpointType, -}; -use lemmy_apub_lib::object_id::ObjectId; -use lemmy_db_schema::{from_opt_str_to_opt_enum, SortType}; +use lemmy_db_schema::{from_opt_str_to_opt_enum, source::person::Person, SortType}; use lemmy_db_views::{comment_view::CommentQueryBuilder, post_view::PostQueryBuilder}; use lemmy_db_views_actor::{ community_moderator_view::CommunityModeratorView, @@ -55,13 +50,9 @@ impl PerformCrud for GetPersonDetails { .username .to_owned() .unwrap_or_else(|| "admin".to_string()); - let actor_id = - webfinger_resolve::(&name, EndpointType::Person, context, &mut 0).await?; - let person = ObjectId::::new(actor_id) - .dereference(context, context.client(), &mut 0) - .await; - person + resolve_actor_identifier::(&name, context.pool()) + .await .map_err(LemmyError::from) .map_err(|e| e.with_message("couldnt_find_that_username_or_email"))? .id diff --git a/crates/apub/src/fetcher/search.rs b/crates/apub/src/fetcher/search.rs index 8c53c25c7..6271cec12 100644 --- a/crates/apub/src/fetcher/search.rs +++ b/crates/apub/src/fetcher/search.rs @@ -1,8 +1,7 @@ use crate::{ - fetcher::webfinger::webfinger_resolve, + fetcher::webfinger::webfinger_resolve_actor, objects::{comment::ApubComment, community::ApubCommunity, person::ApubPerson, post::ApubPost}, protocol::objects::{group::Group, note::Note, page::Page, person::Person}, - EndpointType, }; use chrono::NaiveDateTime; use lemmy_apub_lib::{object_id::ObjectId, traits::ApubObject}; @@ -34,13 +33,8 @@ pub async fn search_by_apub_id( let (kind, identifier) = query.split_at(1); match kind { "@" => { - let id = webfinger_resolve::( - identifier, - EndpointType::Person, - context, - request_counter, - ) - .await?; + let id = + webfinger_resolve_actor::(identifier, context, request_counter).await?; Ok(SearchableObjects::Person( ObjectId::new(id) .dereference(context, context.client(), request_counter) @@ -48,13 +42,8 @@ pub async fn search_by_apub_id( )) } "!" => { - let id = webfinger_resolve::( - identifier, - EndpointType::Community, - context, - request_counter, - ) - .await?; + let id = + webfinger_resolve_actor::(identifier, context, request_counter).await?; Ok(SearchableObjects::Community( ObjectId::new(id) .dereference(context, context.client(), request_counter) diff --git a/crates/apub/src/fetcher/webfinger.rs b/crates/apub/src/fetcher/webfinger.rs index e623bee22..d039c1c17 100644 --- a/crates/apub/src/fetcher/webfinger.rs +++ b/crates/apub/src/fetcher/webfinger.rs @@ -1,4 +1,3 @@ -use crate::{generate_local_apub_endpoint, EndpointType}; use itertools::Itertools; use lemmy_apub_lib::{ object_id::ObjectId, @@ -28,37 +27,6 @@ pub struct WebfingerResponse { pub links: Vec, } -/// Takes in a shortname of the type dessalines@xyz.tld or dessalines (assumed to be local), and -/// outputs the actor id. Used in the API for communities and users. -/// -/// TODO: later provide a method in ApubObject to generate the endpoint, so that we dont have to -/// pass in EndpointType -#[tracing::instrument(skip_all)] -pub async fn webfinger_resolve( - identifier: &str, - endpoint_type: EndpointType, - context: &LemmyContext, - request_counter: &mut i32, -) -> Result -where - Kind: ApubObject + ActorType + Send + 'static, - for<'de2> ::ApubType: serde::Deserialize<'de2>, -{ - // remote actor - if identifier.contains('@') { - webfinger_resolve_actor::(identifier, context, request_counter).await - } - // local actor - else { - let domain = context.settings().get_protocol_and_hostname(); - Ok(generate_local_apub_endpoint( - endpoint_type, - identifier, - &domain, - )?) - } -} - /// Turns a person id like `@name@example.com` into an apub ID, like `https://example.com/user/name`, /// using webfinger. #[tracing::instrument(skip_all)] diff --git a/crates/apub/src/http/community.rs b/crates/apub/src/http/community.rs index 9e1cb2779..5483d58d1 100644 --- a/crates/apub/src/http/community.rs +++ b/crates/apub/src/http/community.rs @@ -24,7 +24,7 @@ use crate::{ use actix_web::{web, web::Payload, HttpRequest, HttpResponse}; use lemmy_api_common::blocking; use lemmy_apub_lib::{object_id::ObjectId, traits::ApubObject}; -use lemmy_db_schema::source::community::Community; +use lemmy_db_schema::{source::community::Community, traits::ApubActor}; use lemmy_utils::LemmyError; use lemmy_websocket::LemmyContext; use serde::Deserialize; diff --git a/crates/apub/src/http/person.rs b/crates/apub/src/http/person.rs index 131c90404..bc0633d8d 100644 --- a/crates/apub/src/http/person.rs +++ b/crates/apub/src/http/person.rs @@ -14,7 +14,7 @@ use crate::{ use actix_web::{web, web::Payload, HttpRequest, HttpResponse}; use lemmy_api_common::blocking; use lemmy_apub_lib::traits::ApubObject; -use lemmy_db_schema::source::person::Person; +use lemmy_db_schema::{source::person::Person, traits::ApubActor}; use lemmy_utils::LemmyError; use lemmy_websocket::LemmyContext; use serde::Deserialize; @@ -34,7 +34,7 @@ pub(crate) async fn get_apub_person_http( let user_name = info.into_inner().user_name; // TODO: this needs to be able to read deleted persons, so that it can send tombstones let person: ApubPerson = blocking(context.pool(), move |conn| { - Person::find_by_name(conn, &user_name) + Person::read_from_name(conn, &user_name) }) .await?? .into(); @@ -77,7 +77,7 @@ pub(crate) async fn get_apub_person_outbox( context: web::Data, ) -> Result { let person = blocking(context.pool(), move |conn| { - Person::find_by_name(conn, &info.user_name) + Person::read_from_name(conn, &info.user_name) }) .await??; let outbox = PersonOutbox::new(person).await?; diff --git a/crates/apub/src/objects/community.rs b/crates/apub/src/objects/community.rs index 65b0e64b1..cce61b57f 100644 --- a/crates/apub/src/objects/community.rs +++ b/crates/apub/src/objects/community.rs @@ -18,7 +18,7 @@ use lemmy_apub_lib::{ traits::{ActorType, ApubObject}, values::MediaTypeMarkdown, }; -use lemmy_db_schema::source::community::Community; +use lemmy_db_schema::{source::community::Community, traits::ApubActor}; use lemmy_db_views_actor::community_follower_view::CommunityFollowerView; use lemmy_utils::{ utils::{convert_datetime, markdown_to_html}, diff --git a/crates/apub/src/objects/person.rs b/crates/apub/src/objects/person.rs index f6ad0e207..21f0f248c 100644 --- a/crates/apub/src/objects/person.rs +++ b/crates/apub/src/objects/person.rs @@ -22,6 +22,7 @@ use lemmy_apub_lib::{ use lemmy_db_schema::{ naive_now, source::person::{Person as DbPerson, PersonForm}, + traits::ApubActor, }; use lemmy_utils::{ utils::{check_slurs, check_slurs_opt, convert_datetime, markdown_to_html}, diff --git a/crates/db_schema/src/impls/community.rs b/crates/db_schema/src/impls/community.rs index 228cf23ff..d2b0d9cde 100644 --- a/crates/db_schema/src/impls/community.rs +++ b/crates/db_schema/src/impls/community.rs @@ -13,9 +13,17 @@ use crate::{ CommunityPersonBanForm, CommunitySafe, }, - traits::{Bannable, Crud, DeleteableOrRemoveable, Followable, Joinable}, + traits::{ApubActor, Bannable, Crud, DeleteableOrRemoveable, Followable, Joinable}, +}; +use diesel::{ + dsl::*, + result::Error, + ExpressionMethods, + PgConnection, + QueryDsl, + RunQueryDsl, + TextExpressionMethods, }; -use diesel::{dsl::*, result::Error, ExpressionMethods, PgConnection, QueryDsl, RunQueryDsl}; use url::Url; mod safe_type { @@ -92,14 +100,6 @@ impl Crud for Community { } impl Community { - pub fn read_from_name(conn: &PgConnection, community_name: &str) -> Result { - use crate::schema::community::dsl::*; - community - .filter(local.eq(true)) - .filter(lower(name).eq(lower(community_name))) - .first::(conn) - } - pub fn update_deleted( conn: &PgConnection, community_id: CommunityId, @@ -136,17 +136,6 @@ impl Community { .set(community_form) .get_result::(conn) } - pub fn read_from_apub_id(conn: &PgConnection, object_id: Url) -> Result, Error> { - use crate::schema::community::dsl::*; - let object_id: DbUrl = object_id.into(); - Ok( - community - .filter(actor_id.eq(object_id)) - .first::(conn) - .ok() - .map(Into::into), - ) - } } impl Joinable for CommunityModerator { @@ -299,6 +288,40 @@ impl Followable for CommunityFollower { } } +impl ApubActor for Community { + fn read_from_apub_id(conn: &PgConnection, object_id: Url) -> Result, Error> { + use crate::schema::community::dsl::*; + let object_id: DbUrl = object_id.into(); + Ok( + community + .filter(actor_id.eq(object_id)) + .first::(conn) + .ok() + .map(Into::into), + ) + } + + fn read_from_name(conn: &PgConnection, community_name: &str) -> Result { + use crate::schema::community::dsl::*; + community + .filter(local.eq(true)) + .filter(lower(name).eq(lower(community_name))) + .first::(conn) + } + + fn read_from_name_and_domain( + conn: &PgConnection, + community_name: &str, + protocol_domain: &str, + ) -> Result { + use crate::schema::community::dsl::*; + community + .filter(lower(name).eq(lower(community_name))) + .filter(actor_id.like(format!("{}%", protocol_domain))) + .first::(conn) + } +} + #[cfg(test)] mod tests { use crate::{ diff --git a/crates/db_schema/src/impls/person.rs b/crates/db_schema/src/impls/person.rs index 17833253b..4c052c8b6 100644 --- a/crates/db_schema/src/impls/person.rs +++ b/crates/db_schema/src/impls/person.rs @@ -4,9 +4,17 @@ use crate::{ newtypes::{DbUrl, PersonId}, schema::person::dsl::*, source::person::{Person, PersonForm, PersonSafe}, - traits::Crud, + traits::{ApubActor, Crud}, +}; +use diesel::{ + dsl::*, + result::Error, + ExpressionMethods, + PgConnection, + QueryDsl, + RunQueryDsl, + TextExpressionMethods, }; -use diesel::{dsl::*, result::Error, ExpressionMethods, PgConnection, QueryDsl, RunQueryDsl}; use url::Url; mod safe_type { @@ -202,14 +210,6 @@ impl Person { .get_result::(conn) } - pub fn find_by_name(conn: &PgConnection, from_name: &str) -> Result { - person - .filter(deleted.eq(false)) - .filter(local.eq(true)) - .filter(lower(name).eq(lower(from_name))) - .first::(conn) - } - pub fn mark_as_updated(conn: &PgConnection, person_id: PersonId) -> Result { diesel::update(person.find(person_id)) .set((last_refreshed_at.eq(naive_now()),)) @@ -247,19 +247,6 @@ impl Person { .get_result::(conn) } - pub fn read_from_apub_id(conn: &PgConnection, object_id: Url) -> Result, Error> { - use crate::schema::person::dsl::*; - let object_id: DbUrl = object_id.into(); - Ok( - person - .filter(deleted.eq(false)) - .filter(actor_id.eq(object_id)) - .first::(conn) - .ok() - .map(Into::into), - ) - } - pub fn update_deleted( conn: &PgConnection, person_id: PersonId, @@ -296,6 +283,41 @@ fn is_banned(banned_: bool, expires: Option) -> bool { } } +impl ApubActor for Person { + fn read_from_apub_id(conn: &PgConnection, object_id: Url) -> Result, Error> { + use crate::schema::person::dsl::*; + let object_id: DbUrl = object_id.into(); + Ok( + person + .filter(deleted.eq(false)) + .filter(actor_id.eq(object_id)) + .first::(conn) + .ok() + .map(Into::into), + ) + } + + fn read_from_name(conn: &PgConnection, from_name: &str) -> Result { + person + .filter(deleted.eq(false)) + .filter(local.eq(true)) + .filter(lower(name).eq(lower(from_name))) + .first::(conn) + } + + fn read_from_name_and_domain( + conn: &PgConnection, + person_name: &str, + protocol_domain: &str, + ) -> Result { + use crate::schema::person::dsl::*; + person + .filter(lower(name).eq(lower(person_name))) + .filter(actor_id.like(format!("{}%", protocol_domain))) + .first::(conn) + } +} + #[cfg(test)] mod tests { use crate::{establish_unpooled_connection, source::person::*, traits::Crud}; diff --git a/crates/db_schema/src/traits.rs b/crates/db_schema/src/traits.rs index aea36ed87..ad984d7bf 100644 --- a/crates/db_schema/src/traits.rs +++ b/crates/db_schema/src/traits.rs @@ -1,5 +1,6 @@ use crate::newtypes::{CommunityId, PersonId}; use diesel::{result::Error, PgConnection}; +use url::Url; pub trait Crud { type Form; @@ -162,3 +163,20 @@ pub trait ViewToVec { where Self: Sized; } + +pub trait ApubActor { + // TODO: this should be in a trait ApubObject (and implemented for Post, Comment, PrivateMessage as well) + fn read_from_apub_id(conn: &PgConnection, object_id: Url) -> Result, Error> + where + Self: Sized; + fn read_from_name(conn: &PgConnection, actor_name: &str) -> Result + where + Self: Sized; + fn read_from_name_and_domain( + conn: &PgConnection, + actor_name: &str, + protocol_domain: &str, + ) -> Result + where + Self: Sized; +} diff --git a/crates/routes/src/feeds.rs b/crates/routes/src/feeds.rs index 7b668d7eb..9538f06c0 100644 --- a/crates/routes/src/feeds.rs +++ b/crates/routes/src/feeds.rs @@ -6,7 +6,7 @@ use lemmy_api_common::blocking; use lemmy_db_schema::{ newtypes::LocalUserId, source::{community::Community, local_user::LocalUser, person::Person}, - traits::Crud, + traits::{ApubActor, Crud}, ListingType, SortType, }; @@ -175,7 +175,7 @@ fn get_feed_user( protocol_and_hostname: &str, ) -> Result { let site_view = SiteView::read(conn)?; - let person = Person::find_by_name(conn, user_name)?; + let person = Person::read_from_name(conn, user_name)?; let posts = PostQueryBuilder::create(conn) .listing_type(ListingType::All) diff --git a/crates/routes/src/webfinger.rs b/crates/routes/src/webfinger.rs index 82b8a619e..b31553793 100644 --- a/crates/routes/src/webfinger.rs +++ b/crates/routes/src/webfinger.rs @@ -2,7 +2,10 @@ use actix_web::{web, web::Query, HttpResponse}; use anyhow::Context; use lemmy_api_common::blocking; use lemmy_apub::fetcher::webfinger::{WebfingerLink, WebfingerResponse}; -use lemmy_db_schema::source::{community::Community, person::Person}; +use lemmy_db_schema::{ + source::{community::Community, person::Person}, + traits::ApubActor, +}; use lemmy_utils::{location_info, settings::structs::Settings, LemmyError}; use lemmy_websocket::LemmyContext; use serde::Deserialize; @@ -44,7 +47,7 @@ async fn get_webfinger_response( let name_ = name.clone(); let user_id: Option = blocking(context.pool(), move |conn| { - Person::find_by_name(conn, &name_) + Person::read_from_name(conn, &name_) }) .await? .ok()