From 8b6860881c6ed1e4844c5950408f3d0cadaf5e6d Mon Sep 17 00:00:00 2001 From: Felix Ableitner Date: Wed, 23 Sep 2020 16:39:30 +0200 Subject: [PATCH] Move apub to separate workspace --- Cargo.lock | 47 ++- Cargo.toml | 4 +- lemmy_apub/Cargo.toml | 43 +++ lemmy_apub/src/activities.rs | 4 +- lemmy_apub/src/activity_queue.rs | 2 +- lemmy_apub/src/comment.rs | 34 +- lemmy_apub/src/community.rs | 30 +- lemmy_apub/src/extensions/signatures.rs | 2 +- lemmy_apub/src/fetcher.rs | 24 +- lemmy_apub/src/inbox/activities/announce.rs | 2 +- lemmy_apub/src/inbox/activities/create.rs | 2 +- lemmy_apub/src/inbox/activities/delete.rs | 2 +- lemmy_apub/src/inbox/activities/dislike.rs | 2 +- lemmy_apub/src/inbox/activities/like.rs | 2 +- lemmy_apub/src/inbox/activities/remove.rs | 2 +- lemmy_apub/src/inbox/activities/undo.rs | 2 +- lemmy_apub/src/inbox/activities/update.rs | 2 +- lemmy_apub/src/inbox/community_inbox.rs | 2 +- lemmy_apub/src/inbox/shared_inbox.rs | 10 +- lemmy_apub/src/inbox/user_inbox.rs | 2 +- lemmy_apub/src/lib.rs | 363 ++++++++++++++++++++ lemmy_apub/src/post.rs | 30 +- lemmy_apub/src/private_message.rs | 26 +- lemmy_apub/src/user.rs | 24 +- lemmy_structs/Cargo.toml | 1 - lemmy_structs/src/lib.rs | 18 + lemmy_utils/Cargo.toml | 1 + lemmy_utils/src/lib.rs | 1 + lemmy_utils/src/request.rs | 2 +- src/api/comment.rs | 18 +- src/api/community.rs | 6 +- src/api/post.rs | 2 +- src/api/site.rs | 2 +- src/api/user.rs | 2 +- src/lib.rs | 12 +- src/main.rs | 2 +- src/routes/federation.rs | 6 +- src/routes/webfinger.rs | 21 +- 38 files changed, 597 insertions(+), 160 deletions(-) create mode 100644 lemmy_apub/Cargo.toml create mode 100644 lemmy_apub/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 1044e7653..69be8e8d1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1787,6 +1787,48 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +[[package]] +name = "lemmy_apub" +version = "0.1.0" +dependencies = [ + "activitystreams", + "activitystreams-ext", + "actix", + "actix-rt", + "actix-web", + "anyhow", + "async-trait", + "awc", + "background-jobs", + "base64 0.12.3", + "bcrypt", + "chrono", + "diesel", + "futures", + "http", + "http-signature-normalization-actix", + "itertools", + "lazy_static", + "lemmy_db", + "lemmy_structs", + "lemmy_utils", + "lemmy_websocket", + "log", + "openssl", + "percent-encoding", + "rand 0.7.3", + "reqwest", + "serde 1.0.116", + "serde_json", + "sha2", + "strum", + "strum_macros", + "thiserror", + "tokio", + "url", + "uuid 0.8.1", +] + [[package]] name = "lemmy_db" version = "0.1.0" @@ -1822,8 +1864,6 @@ dependencies = [ name = "lemmy_server" version = "0.0.1" dependencies = [ - "activitystreams", - "activitystreams-ext", "actix", "actix-files", "actix-rt", @@ -1847,6 +1887,7 @@ dependencies = [ "itertools", "jsonwebtoken", "lazy_static", + "lemmy_apub", "lemmy_db", "lemmy_rate_limit", "lemmy_structs", @@ -1873,7 +1914,6 @@ dependencies = [ name = "lemmy_structs" version = "0.1.0" dependencies = [ - "actix", "actix-web", "chrono", "diesel", @@ -1901,6 +1941,7 @@ dependencies = [ "openssl", "rand 0.7.3", "regex", + "reqwest", "serde 1.0.116", "serde_json", "thiserror", diff --git a/Cargo.toml b/Cargo.toml index eb7756008..ccfc4742d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ lto = true [workspace] members = [ + "lemmy_apub", "lemmy_utils", "lemmy_db", "lemmy_structs", @@ -21,10 +22,9 @@ lemmy_db = { path = "./lemmy_db" } lemmy_structs = { path = "./lemmy_structs" } lemmy_rate_limit = { path = "./lemmy_rate_limit" } lemmy_websocket = { path = "./lemmy_websocket" } +lemmy_apub = { path = "./lemmy_apub" } diesel = "1.4" diesel_migrations = "1.4" -activitystreams = "0.7.0-alpha.4" -activitystreams-ext = "0.1.0-alpha.2" bcrypt = "0.8" chrono = { version = "0.4", features = ["serde"] } serde_json = { version = "1.0", features = ["preserve_order"]} diff --git a/lemmy_apub/Cargo.toml b/lemmy_apub/Cargo.toml new file mode 100644 index 000000000..5e05b1c97 --- /dev/null +++ b/lemmy_apub/Cargo.toml @@ -0,0 +1,43 @@ +[package] +name = "lemmy_apub" +version = "0.1.0" +authors = ["Felix Ableitner "] +edition = "2018" + +[dependencies] +lemmy_utils = { path = "../lemmy_utils" } +lemmy_db = { path = "../lemmy_db" } +lemmy_structs = { path = "../lemmy_structs" } +lemmy_websocket = { path = "../lemmy_websocket" } +diesel = "1.4" +activitystreams = "0.7.0-alpha.4" +activitystreams-ext = "0.1.0-alpha.2" +bcrypt = "0.8" +chrono = { version = "0.4", features = ["serde"] } +serde_json = { version = "1.0", features = ["preserve_order"]} +serde = { version = "1.0", features = ["derive"] } +actix = "0.10" +actix-web = { version = "3.0", default-features = false } +actix-rt = { version = "1.1", default-features = false } +awc = { version = "2.0", default-features = false } +log = "0.4" +rand = "0.7" +strum = "0.19" +strum_macros = "0.19" +lazy_static = "1.3" +url = { version = "2.1", features = ["serde"] } +percent-encoding = "2.1" +openssl = "0.10" +http = "0.2" +http-signature-normalization-actix = { version = "0.4", default-features = false, features = ["sha-2"] } +base64 = "0.12" +tokio = "0.2" +futures = "0.3" +itertools = "0.9" +uuid = { version = "0.8", features = ["serde", "v4"] } +sha2 = "0.9" +async-trait = "0.1" +anyhow = "1.0" +thiserror = "1.0" +background-jobs = " 0.8" +reqwest = { version = "0.10", features = ["json"] } \ No newline at end of file diff --git a/lemmy_apub/src/activities.rs b/lemmy_apub/src/activities.rs index ee28a8d37..3b1b12ab3 100644 --- a/lemmy_apub/src/activities.rs +++ b/lemmy_apub/src/activities.rs @@ -1,4 +1,4 @@ -use crate::apub::{activity_queue::send_activity, community::do_announce, insert_activity}; +use crate::{activity_queue::send_activity, community::do_announce, insert_activity}; use activitystreams::{ base::{Extends, ExtendsExt}, object::AsObject, @@ -35,7 +35,7 @@ where Ok(()) } -pub(in crate::apub) fn generate_activity_id(kind: T) -> Result +pub(in crate) fn generate_activity_id(kind: T) -> Result where T: ToString, { diff --git a/lemmy_apub/src/activity_queue.rs b/lemmy_apub/src/activity_queue.rs index 008846bf3..e3af7677c 100644 --- a/lemmy_apub/src/activity_queue.rs +++ b/lemmy_apub/src/activity_queue.rs @@ -1,4 +1,4 @@ -use crate::apub::{check_is_apub_id_valid, extensions::signatures::sign, ActorType}; +use crate::{check_is_apub_id_valid, extensions::signatures::sign, ActorType}; use activitystreams::{ base::{Extends, ExtendsExt}, object::AsObject, diff --git a/lemmy_apub/src/comment.rs b/lemmy_apub/src/comment.rs index 763ae9b00..4e5c173f8 100644 --- a/lemmy_apub/src/comment.rs +++ b/lemmy_apub/src/comment.rs @@ -1,23 +1,20 @@ use crate::{ - apub::{ - activities::{generate_activity_id, send_activity_to_community}, - check_actor_domain, - create_apub_response, - create_apub_tombstone_response, - create_tombstone, - fetch_webfinger_url, - fetcher::{ - get_or_fetch_and_insert_comment, - get_or_fetch_and_insert_post, - get_or_fetch_and_upsert_user, - }, - ActorType, - ApubLikeableType, - ApubObjectType, - FromApub, - ToApub, + activities::{generate_activity_id, send_activity_to_community}, + check_actor_domain, + create_apub_response, + create_apub_tombstone_response, + create_tombstone, + fetch_webfinger_url, + fetcher::{ + get_or_fetch_and_insert_comment, + get_or_fetch_and_insert_post, + get_or_fetch_and_upsert_user, }, - DbPool, + ActorType, + ApubLikeableType, + ApubObjectType, + FromApub, + ToApub, }; use activitystreams::{ activity::{ @@ -45,6 +42,7 @@ use lemmy_db::{ post::Post, user::User_, Crud, + DbPool, }; use lemmy_structs::blocking; use lemmy_utils::{ diff --git a/lemmy_apub/src/community.rs b/lemmy_apub/src/community.rs index b68d5bd15..8a41e8866 100644 --- a/lemmy_apub/src/community.rs +++ b/lemmy_apub/src/community.rs @@ -1,20 +1,17 @@ use crate::{ - apub::{ - activities::generate_activity_id, - activity_queue::send_activity, - check_actor_domain, - create_apub_response, - create_apub_tombstone_response, - create_tombstone, - extensions::group_extensions::GroupExtension, - fetcher::{get_or_fetch_and_upsert_actor, get_or_fetch_and_upsert_user}, - insert_activity, - ActorType, - FromApub, - GroupExt, - ToApub, - }, - DbPool, + activities::generate_activity_id, + activity_queue::send_activity, + check_actor_domain, + create_apub_response, + create_apub_tombstone_response, + create_tombstone, + extensions::group_extensions::GroupExtension, + fetcher::{get_or_fetch_and_upsert_actor, get_or_fetch_and_upsert_user}, + insert_activity, + ActorType, + FromApub, + GroupExt, + ToApub, }; use activitystreams::{ activity::{ @@ -43,6 +40,7 @@ use lemmy_db::{ naive_now, post::Post, user::User_, + DbPool, }; use lemmy_structs::blocking; use lemmy_utils::{ diff --git a/lemmy_apub/src/extensions/signatures.rs b/lemmy_apub/src/extensions/signatures.rs index 4a261c17e..5471e19e4 100644 --- a/lemmy_apub/src/extensions/signatures.rs +++ b/lemmy_apub/src/extensions/signatures.rs @@ -1,4 +1,4 @@ -use crate::apub::ActorType; +use crate::ActorType; use activitystreams::unparsed::UnparsedMutExt; use activitystreams_ext::UnparsedExtension; use actix_web::{client::ClientRequest, HttpRequest}; diff --git a/lemmy_apub/src/fetcher.rs b/lemmy_apub/src/fetcher.rs index 0f1f1663a..4ce4082cd 100644 --- a/lemmy_apub/src/fetcher.rs +++ b/lemmy_apub/src/fetcher.rs @@ -1,14 +1,11 @@ use crate::{ - apub::{ - check_is_apub_id_valid, - ActorType, - FromApub, - GroupExt, - PageExt, - PersonExt, - APUB_JSON_CONTENT_TYPE, - }, - request::{retry, RecvError}, + check_is_apub_id_valid, + ActorType, + FromApub, + GroupExt, + PageExt, + PersonExt, + APUB_JSON_CONTENT_TYPE, }; use activitystreams::{base::BaseExt, collection::OrderedCollection, object::Note, prelude::*}; use anyhow::{anyhow, Context}; @@ -29,7 +26,12 @@ use lemmy_db::{ SearchType, }; use lemmy_structs::{blocking, site::SearchResponse}; -use lemmy_utils::{apub::get_apub_protocol_string, location_info, LemmyError}; +use lemmy_utils::{ + apub::get_apub_protocol_string, + location_info, + request::{retry, RecvError}, + LemmyError, +}; use lemmy_websocket::LemmyContext; use log::debug; use reqwest::Client; diff --git a/lemmy_apub/src/inbox/activities/announce.rs b/lemmy_apub/src/inbox/activities/announce.rs index 52b571994..d861e5f27 100644 --- a/lemmy_apub/src/inbox/activities/announce.rs +++ b/lemmy_apub/src/inbox/activities/announce.rs @@ -1,4 +1,4 @@ -use crate::apub::inbox::{ +use crate::inbox::{ activities::{ create::receive_create, delete::receive_delete, diff --git a/lemmy_apub/src/inbox/activities/create.rs b/lemmy_apub/src/inbox/activities/create.rs index 60b02c585..e25fdd979 100644 --- a/lemmy_apub/src/inbox/activities/create.rs +++ b/lemmy_apub/src/inbox/activities/create.rs @@ -1,4 +1,4 @@ -use crate::apub::{ +use crate::{ inbox::shared_inbox::{ announce_if_community_is_local, get_user_from_activity, diff --git a/lemmy_apub/src/inbox/activities/delete.rs b/lemmy_apub/src/inbox/activities/delete.rs index 5cbe004e5..2c3760e42 100644 --- a/lemmy_apub/src/inbox/activities/delete.rs +++ b/lemmy_apub/src/inbox/activities/delete.rs @@ -1,4 +1,4 @@ -use crate::apub::{ +use crate::{ fetcher::{get_or_fetch_and_insert_comment, get_or_fetch_and_insert_post}, inbox::shared_inbox::{ announce_if_community_is_local, diff --git a/lemmy_apub/src/inbox/activities/dislike.rs b/lemmy_apub/src/inbox/activities/dislike.rs index a1f23f475..dd63011d4 100644 --- a/lemmy_apub/src/inbox/activities/dislike.rs +++ b/lemmy_apub/src/inbox/activities/dislike.rs @@ -1,4 +1,4 @@ -use crate::apub::{ +use crate::{ fetcher::{get_or_fetch_and_insert_comment, get_or_fetch_and_insert_post}, inbox::shared_inbox::{ announce_if_community_is_local, diff --git a/lemmy_apub/src/inbox/activities/like.rs b/lemmy_apub/src/inbox/activities/like.rs index 8a9e2a675..7b56867b4 100644 --- a/lemmy_apub/src/inbox/activities/like.rs +++ b/lemmy_apub/src/inbox/activities/like.rs @@ -1,4 +1,4 @@ -use crate::apub::{ +use crate::{ fetcher::{get_or_fetch_and_insert_comment, get_or_fetch_and_insert_post}, inbox::shared_inbox::{ announce_if_community_is_local, diff --git a/lemmy_apub/src/inbox/activities/remove.rs b/lemmy_apub/src/inbox/activities/remove.rs index 2b018dbff..27a7775e6 100644 --- a/lemmy_apub/src/inbox/activities/remove.rs +++ b/lemmy_apub/src/inbox/activities/remove.rs @@ -1,4 +1,4 @@ -use crate::apub::{ +use crate::{ fetcher::{get_or_fetch_and_insert_comment, get_or_fetch_and_insert_post}, inbox::shared_inbox::{ announce_if_community_is_local, diff --git a/lemmy_apub/src/inbox/activities/undo.rs b/lemmy_apub/src/inbox/activities/undo.rs index 7268e270b..9a421ee69 100644 --- a/lemmy_apub/src/inbox/activities/undo.rs +++ b/lemmy_apub/src/inbox/activities/undo.rs @@ -1,4 +1,4 @@ -use crate::apub::{ +use crate::{ fetcher::{get_or_fetch_and_insert_comment, get_or_fetch_and_insert_post}, inbox::shared_inbox::{ announce_if_community_is_local, diff --git a/lemmy_apub/src/inbox/activities/update.rs b/lemmy_apub/src/inbox/activities/update.rs index acf51539a..17d9d7084 100644 --- a/lemmy_apub/src/inbox/activities/update.rs +++ b/lemmy_apub/src/inbox/activities/update.rs @@ -1,4 +1,4 @@ -use crate::apub::{ +use crate::{ fetcher::{get_or_fetch_and_insert_comment, get_or_fetch_and_insert_post}, inbox::shared_inbox::{ announce_if_community_is_local, diff --git a/lemmy_apub/src/inbox/community_inbox.rs b/lemmy_apub/src/inbox/community_inbox.rs index 600f2d5d3..ee75fa005 100644 --- a/lemmy_apub/src/inbox/community_inbox.rs +++ b/lemmy_apub/src/inbox/community_inbox.rs @@ -1,4 +1,4 @@ -use crate::apub::{ +use crate::{ check_is_apub_id_valid, extensions::signatures::verify, fetcher::get_or_fetch_and_upsert_user, diff --git a/lemmy_apub/src/inbox/shared_inbox.rs b/lemmy_apub/src/inbox/shared_inbox.rs index 966a19454..b9077ebe9 100644 --- a/lemmy_apub/src/inbox/shared_inbox.rs +++ b/lemmy_apub/src/inbox/shared_inbox.rs @@ -1,4 +1,4 @@ -use crate::apub::{ +use crate::{ check_is_apub_id_valid, community::do_announce, extensions::signatures::verify, @@ -95,7 +95,7 @@ pub async fn shared_inbox( res } -pub(in crate::apub::inbox) fn receive_unhandled_activity( +pub(in crate::inbox) fn receive_unhandled_activity( activity: A, ) -> Result where @@ -105,7 +105,7 @@ where Ok(HttpResponse::NotImplemented().finish()) } -pub(in crate::apub::inbox) async fn get_user_from_activity( +pub(in crate::inbox) async fn get_user_from_activity( activity: &T, context: &LemmyContext, ) -> Result @@ -117,7 +117,7 @@ where get_or_fetch_and_upsert_user(&user_uri, context).await } -pub(in crate::apub::inbox) fn get_community_id_from_activity( +pub(in crate::inbox) fn get_community_id_from_activity( activity: &T, ) -> Result where @@ -134,7 +134,7 @@ where ) } -pub(in crate::apub::inbox) async fn announce_if_community_is_local( +pub(in crate::inbox) async fn announce_if_community_is_local( activity: T, user: &User_, context: &LemmyContext, diff --git a/lemmy_apub/src/inbox/user_inbox.rs b/lemmy_apub/src/inbox/user_inbox.rs index 0376e2b10..a7050c45c 100644 --- a/lemmy_apub/src/inbox/user_inbox.rs +++ b/lemmy_apub/src/inbox/user_inbox.rs @@ -1,4 +1,4 @@ -use crate::apub::{ +use crate::{ check_is_apub_id_valid, extensions::signatures::verify, fetcher::{get_or_fetch_and_upsert_actor, get_or_fetch_and_upsert_community}, diff --git a/lemmy_apub/src/lib.rs b/lemmy_apub/src/lib.rs new file mode 100644 index 000000000..22eb9fbe0 --- /dev/null +++ b/lemmy_apub/src/lib.rs @@ -0,0 +1,363 @@ +#[macro_use] +extern crate lazy_static; + +pub mod activities; +pub mod activity_queue; +pub mod comment; +pub mod community; +pub mod extensions; +pub mod fetcher; +pub mod inbox; +pub mod post; +pub mod private_message; +pub mod user; + +use crate::extensions::{ + group_extensions::GroupExtension, + page_extension::PageExtension, + signatures::{PublicKey, PublicKeyExtension}, +}; +use activitystreams::{ + activity::Follow, + actor::{ApActor, Group, Person}, + base::AsBase, + markers::Base, + object::{Page, Tombstone}, + prelude::*, +}; +use activitystreams_ext::{Ext1, Ext2}; +use actix_web::{body::Body, HttpResponse}; +use anyhow::{anyhow, Context}; +use chrono::NaiveDateTime; +use lemmy_db::{activity::do_insert_activity, user::User_, DbPool}; +use lemmy_structs::{blocking, WebFingerResponse}; +use lemmy_utils::{ + apub::get_apub_protocol_string, + location_info, + request::{retry, RecvError}, + settings::Settings, + utils::{convert_datetime, MentionData}, + LemmyError, +}; +use lemmy_websocket::LemmyContext; +use log::debug; +use reqwest::Client; +use serde::Serialize; +use url::{ParseError, Url}; + +type GroupExt = Ext2, GroupExtension, PublicKeyExtension>; +type PersonExt = Ext1, PublicKeyExtension>; +type PageExt = Ext1; + +pub static APUB_JSON_CONTENT_TYPE: &str = "application/activity+json"; + +/// Convert the data to json and turn it into an HTTP Response with the correct ActivityPub +/// headers. +fn create_apub_response(data: &T) -> HttpResponse +where + T: Serialize, +{ + HttpResponse::Ok() + .content_type(APUB_JSON_CONTENT_TYPE) + .json(data) +} + +fn create_apub_tombstone_response(data: &T) -> HttpResponse +where + T: Serialize, +{ + HttpResponse::Gone() + .content_type(APUB_JSON_CONTENT_TYPE) + .json(data) +} + +// Checks if the ID has a valid format, correct scheme, and is in the allowed instance list. +fn check_is_apub_id_valid(apub_id: &Url) -> Result<(), LemmyError> { + let settings = Settings::get(); + let domain = apub_id.domain().context(location_info!())?.to_string(); + let local_instance = settings + .hostname + .split(':') + .collect::>() + .first() + .context(location_info!())? + .to_string(); + + if !settings.federation.enabled { + return if domain == local_instance { + Ok(()) + } else { + Err( + anyhow!( + "Trying to connect with {}, but federation is disabled", + domain + ) + .into(), + ) + }; + } + + if apub_id.scheme() != get_apub_protocol_string() { + return Err(anyhow!("invalid apub id scheme: {:?}", apub_id.scheme()).into()); + } + + let mut allowed_instances = Settings::get().get_allowed_instances(); + let blocked_instances = Settings::get().get_blocked_instances(); + + if !allowed_instances.is_empty() { + // need to allow this explicitly because apub activities might contain objects from our local + // instance. split is needed to remove the port in our federation test setup. + allowed_instances.push(local_instance); + + if allowed_instances.contains(&domain) { + Ok(()) + } else { + Err(anyhow!("{} not in federation allowlist", domain).into()) + } + } else if !blocked_instances.is_empty() { + if blocked_instances.contains(&domain) { + Err(anyhow!("{} is in federation blocklist", domain).into()) + } else { + Ok(()) + } + } else { + panic!("Invalid config, both allowed_instances and blocked_instances are specified"); + } +} + +#[async_trait::async_trait(?Send)] +pub trait ToApub { + type Response; + async fn to_apub(&self, pool: &DbPool) -> Result; + fn to_tombstone(&self) -> Result; +} + +/// Updated is actually the deletion time +fn create_tombstone( + deleted: bool, + object_id: &str, + updated: Option, + former_type: T, +) -> Result +where + T: ToString, +{ + if deleted { + if let Some(updated) = updated { + let mut tombstone = Tombstone::new(); + tombstone.set_id(object_id.parse()?); + tombstone.set_former_type(former_type.to_string()); + tombstone.set_deleted(convert_datetime(updated)); + Ok(tombstone) + } else { + Err(anyhow!("Cant convert to tombstone because updated time was None.").into()) + } + } else { + Err(anyhow!("Cant convert object to tombstone if it wasnt deleted").into()) + } +} + +#[async_trait::async_trait(?Send)] +pub trait FromApub { + type ApubType; + /// Converts an object from ActivityPub type to Lemmy internal type. + /// + /// * `apub` The object to read from + /// * `context` LemmyContext which holds DB pool, HTTP client etc + /// * `expected_domain` If present, ensure that the apub object comes from the same domain as + /// this URL + /// + async fn from_apub( + apub: &Self::ApubType, + context: &LemmyContext, + expected_domain: Option, + ) -> Result + where + Self: Sized; +} + +#[async_trait::async_trait(?Send)] +pub trait ApubObjectType { + async fn send_create(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError>; + async fn send_update(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError>; + async fn send_delete(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError>; + async fn send_undo_delete( + &self, + creator: &User_, + context: &LemmyContext, + ) -> Result<(), LemmyError>; + async fn send_remove(&self, mod_: &User_, context: &LemmyContext) -> Result<(), LemmyError>; + async fn send_undo_remove(&self, mod_: &User_, context: &LemmyContext) -> Result<(), LemmyError>; +} + +pub(in crate) fn check_actor_domain( + apub: &T, + expected_domain: Option, +) -> Result +where + T: Base + AsBase, +{ + let actor_id = if let Some(url) = expected_domain { + let domain = url.domain().context(location_info!())?; + apub.id(domain)?.context(location_info!())? + } else { + let actor_id = apub.id_unchecked().context(location_info!())?; + check_is_apub_id_valid(&actor_id)?; + actor_id + }; + Ok(actor_id.to_string()) +} + +#[async_trait::async_trait(?Send)] +pub trait ApubLikeableType { + async fn send_like(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError>; + async fn send_dislike(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError>; + async fn send_undo_like(&self, creator: &User_, context: &LemmyContext) + -> Result<(), LemmyError>; +} + +#[async_trait::async_trait(?Send)] +pub trait ActorType { + fn actor_id_str(&self) -> String; + + // TODO: every actor should have a public key, so this shouldnt be an option (needs to be fixed in db) + fn public_key(&self) -> Option; + fn private_key(&self) -> Option; + + /// numeric id in the database, used for insert_activity + fn user_id(&self) -> i32; + + // These two have default impls, since currently a community can't follow anything, + // and a user can't be followed (yet) + #[allow(unused_variables)] + async fn send_follow( + &self, + follow_actor_id: &Url, + context: &LemmyContext, + ) -> Result<(), LemmyError>; + async fn send_unfollow( + &self, + follow_actor_id: &Url, + context: &LemmyContext, + ) -> Result<(), LemmyError>; + + #[allow(unused_variables)] + async fn send_accept_follow( + &self, + follow: Follow, + context: &LemmyContext, + ) -> Result<(), LemmyError>; + + async fn send_delete(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError>; + async fn send_undo_delete( + &self, + creator: &User_, + context: &LemmyContext, + ) -> Result<(), LemmyError>; + + async fn send_remove(&self, mod_: &User_, context: &LemmyContext) -> Result<(), LemmyError>; + async fn send_undo_remove(&self, mod_: &User_, context: &LemmyContext) -> Result<(), LemmyError>; + + /// For a given community, returns the inboxes of all followers. + async fn get_follower_inboxes(&self, pool: &DbPool) -> Result, LemmyError>; + + fn actor_id(&self) -> Result { + Url::parse(&self.actor_id_str()) + } + + // TODO move these to the db rows + fn get_inbox_url(&self) -> Result { + Url::parse(&format!("{}/inbox", &self.actor_id_str())) + } + + fn get_shared_inbox_url(&self) -> Result { + let actor_id = self.actor_id()?; + let url = format!( + "{}://{}{}/inbox", + &actor_id.scheme(), + &actor_id.host_str().context(location_info!())?, + if let Some(port) = actor_id.port() { + format!(":{}", port) + } else { + "".to_string() + }, + ); + Ok(Url::parse(&url)?) + } + + fn get_outbox_url(&self) -> Result { + Url::parse(&format!("{}/outbox", &self.actor_id_str())) + } + + fn get_followers_url(&self) -> Result { + Url::parse(&format!("{}/followers", &self.actor_id_str())) + } + + fn get_following_url(&self) -> String { + format!("{}/following", &self.actor_id_str()) + } + + fn get_liked_url(&self) -> String { + format!("{}/liked", &self.actor_id_str()) + } + + fn get_public_key_ext(&self) -> Result { + Ok( + PublicKey { + id: format!("{}#main-key", self.actor_id_str()), + owner: self.actor_id_str(), + public_key_pem: self.public_key().context(location_info!())?, + } + .to_ext(), + ) + } +} + +pub async fn fetch_webfinger_url( + mention: &MentionData, + client: &Client, +) -> Result { + let fetch_url = format!( + "{}://{}/.well-known/webfinger?resource=acct:{}@{}", + get_apub_protocol_string(), + mention.domain, + mention.name, + mention.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() + .map(|u| Url::parse(&u)) + .transpose()? + .ok_or_else(|| anyhow!("No href found.").into()) +} + +pub async fn insert_activity( + user_id: i32, + data: T, + local: bool, + pool: &DbPool, +) -> Result<(), LemmyError> +where + T: Serialize + std::fmt::Debug + Send + 'static, +{ + blocking(pool, move |conn| { + do_insert_activity(conn, user_id, &data, local) + }) + .await??; + Ok(()) +} diff --git a/lemmy_apub/src/post.rs b/lemmy_apub/src/post.rs index d6fb6f0bf..07ecb8f70 100644 --- a/lemmy_apub/src/post.rs +++ b/lemmy_apub/src/post.rs @@ -1,20 +1,17 @@ use crate::{ - apub::{ - activities::{generate_activity_id, send_activity_to_community}, - check_actor_domain, - create_apub_response, - create_apub_tombstone_response, - create_tombstone, - extensions::page_extension::PageExtension, - fetcher::{get_or_fetch_and_upsert_community, get_or_fetch_and_upsert_user}, - ActorType, - ApubLikeableType, - ApubObjectType, - FromApub, - PageExt, - ToApub, - }, - DbPool, + activities::{generate_activity_id, send_activity_to_community}, + check_actor_domain, + create_apub_response, + create_apub_tombstone_response, + create_tombstone, + extensions::page_extension::PageExtension, + fetcher::{get_or_fetch_and_upsert_community, get_or_fetch_and_upsert_user}, + ActorType, + ApubLikeableType, + ApubObjectType, + FromApub, + PageExt, + ToApub, }; use activitystreams::{ activity::{ @@ -39,6 +36,7 @@ use lemmy_db::{ post::{Post, PostForm}, user::User_, Crud, + DbPool, }; use lemmy_structs::blocking; use lemmy_utils::{ diff --git a/lemmy_apub/src/private_message.rs b/lemmy_apub/src/private_message.rs index a5aed5565..d61a7771e 100644 --- a/lemmy_apub/src/private_message.rs +++ b/lemmy_apub/src/private_message.rs @@ -1,18 +1,15 @@ use crate::{ - apub::{ - activities::generate_activity_id, - activity_queue::send_activity, - check_actor_domain, - check_is_apub_id_valid, - create_tombstone, - fetcher::get_or_fetch_and_upsert_user, - insert_activity, - ActorType, - ApubObjectType, - FromApub, - ToApub, - }, - DbPool, + activities::generate_activity_id, + activity_queue::send_activity, + check_actor_domain, + check_is_apub_id_valid, + create_tombstone, + fetcher::get_or_fetch_and_upsert_user, + insert_activity, + ActorType, + ApubObjectType, + FromApub, + ToApub, }; use activitystreams::{ activity::{ @@ -30,6 +27,7 @@ use lemmy_db::{ private_message::{PrivateMessage, PrivateMessageForm}, user::User_, Crud, + DbPool, }; use lemmy_structs::blocking; use lemmy_utils::{location_info, utils::convert_datetime, LemmyError}; diff --git a/lemmy_apub/src/user.rs b/lemmy_apub/src/user.rs index e6846da22..19b096ed0 100644 --- a/lemmy_apub/src/user.rs +++ b/lemmy_apub/src/user.rs @@ -1,17 +1,14 @@ use crate::{ - apub::{ - activities::generate_activity_id, - activity_queue::send_activity, - check_actor_domain, - create_apub_response, - fetcher::get_or_fetch_and_upsert_actor, - insert_activity, - ActorType, - FromApub, - PersonExt, - ToApub, - }, - DbPool, + activities::generate_activity_id, + activity_queue::send_activity, + check_actor_domain, + create_apub_response, + fetcher::get_or_fetch_and_upsert_actor, + insert_activity, + ActorType, + FromApub, + PersonExt, + ToApub, }; use activitystreams::{ activity::{ @@ -29,6 +26,7 @@ use anyhow::Context; use lemmy_db::{ naive_now, user::{UserForm, User_}, + DbPool, }; use lemmy_structs::blocking; use lemmy_utils::{ diff --git a/lemmy_structs/Cargo.toml b/lemmy_structs/Cargo.toml index 6ba1dc802..8cf522c39 100644 --- a/lemmy_structs/Cargo.toml +++ b/lemmy_structs/Cargo.toml @@ -14,7 +14,6 @@ lemmy_utils = { path = "../lemmy_utils" } serde = { version = "1.0", features = ["derive"] } log = "0.4" diesel = "1.4" -actix = "0.10" actix-web = { version = "3.0" } chrono = { version = "0.4", features = ["serde"] } serde_json = { version = "1.0", features = ["preserve_order"]} diff --git a/lemmy_structs/src/lib.rs b/lemmy_structs/src/lib.rs index 8d358d646..3efe0bead 100644 --- a/lemmy_structs/src/lib.rs +++ b/lemmy_structs/src/lib.rs @@ -16,6 +16,24 @@ use lemmy_db::{ }; use lemmy_utils::{email::send_email, settings::Settings, utils::MentionData, LemmyError}; use log::error; +use serde::{Deserialize, Serialize}; + +#[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 diff --git a/lemmy_utils/Cargo.toml b/lemmy_utils/Cargo.toml index 7cb227dc4..b495c641c 100644 --- a/lemmy_utils/Cargo.toml +++ b/lemmy_utils/Cargo.toml @@ -27,3 +27,4 @@ openssl = "0.10" url = { version = "2.1", features = ["serde"] } actix-web = {version = "3.0", default-features = false } anyhow = "1.0" +reqwest = { version = "0.10", features = ["json"] } diff --git a/lemmy_utils/src/lib.rs b/lemmy_utils/src/lib.rs index 9e2c7d4df..49402920f 100644 --- a/lemmy_utils/src/lib.rs +++ b/lemmy_utils/src/lib.rs @@ -3,6 +3,7 @@ extern crate lazy_static; pub mod apub; pub mod email; +pub mod request; pub mod settings; #[cfg(test)] mod test; diff --git a/lemmy_utils/src/request.rs b/lemmy_utils/src/request.rs index 137848f2a..490609e7d 100644 --- a/lemmy_utils/src/request.rs +++ b/lemmy_utils/src/request.rs @@ -1,5 +1,5 @@ +use crate::LemmyError; use anyhow::anyhow; -use lemmy_utils::LemmyError; use std::future::Future; use thiserror::Error; diff --git a/src/api/comment.rs b/src/api/comment.rs index 5ad69c2ea..c169f01f5 100644 --- a/src/api/comment.rs +++ b/src/api/comment.rs @@ -1,15 +1,13 @@ -use crate::{ - api::{ - check_community_ban, - get_post, - get_user_from_jwt, - get_user_from_jwt_opt, - is_mod_or_admin, - Perform, - }, - apub::{ApubLikeableType, ApubObjectType}, +use crate::api::{ + check_community_ban, + get_post, + get_user_from_jwt, + get_user_from_jwt_opt, + is_mod_or_admin, + Perform, }; use actix_web::web::Data; +use lemmy_apub::{ApubLikeableType, ApubObjectType}; use lemmy_db::{ comment::*, comment_view::*, diff --git a/src/api/community.rs b/src/api/community.rs index c85f7c70e..0c414f1b6 100644 --- a/src/api/community.rs +++ b/src/api/community.rs @@ -1,9 +1,7 @@ -use crate::{ - api::{get_user_from_jwt, get_user_from_jwt_opt, is_admin, is_mod_or_admin, Perform}, - apub::ActorType, -}; +use crate::api::{get_user_from_jwt, get_user_from_jwt_opt, is_admin, is_mod_or_admin, Perform}; use actix_web::web::Data; use anyhow::Context; +use lemmy_apub::ActorType; use lemmy_db::{ comment::Comment, comment_view::CommentQueryBuilder, diff --git a/src/api/post.rs b/src/api/post.rs index fb44d4101..debf5d7b5 100644 --- a/src/api/post.rs +++ b/src/api/post.rs @@ -1,9 +1,9 @@ use crate::{ api::{check_community_ban, get_user_from_jwt, get_user_from_jwt_opt, is_mod_or_admin, Perform}, - apub::{ApubLikeableType, ApubObjectType}, fetch_iframely_and_pictrs_data, }; use actix_web::web::Data; +use lemmy_apub::{ApubLikeableType, ApubObjectType}; use lemmy_db::{ comment_view::*, community_view::*, diff --git a/src/api/site.rs b/src/api/site.rs index a59f1f667..963ac1c53 100644 --- a/src/api/site.rs +++ b/src/api/site.rs @@ -1,10 +1,10 @@ use crate::{ api::{get_user_from_jwt, get_user_from_jwt_opt, is_admin, Perform}, - apub::fetcher::search_by_apub_id, version, }; use actix_web::web::Data; use anyhow::Context; +use lemmy_apub::fetcher::search_by_apub_id; use lemmy_db::{ category::*, comment_view::*, diff --git a/src/api/user.rs b/src/api/user.rs index c7977b810..1e06fcfa9 100644 --- a/src/api/user.rs +++ b/src/api/user.rs @@ -1,6 +1,5 @@ use crate::{ api::{claims::Claims, get_user_from_jwt, get_user_from_jwt_opt, is_admin, Perform}, - apub::ApubObjectType, captcha_espeak_wav_base64, }; use actix_web::web::Data; @@ -8,6 +7,7 @@ use anyhow::Context; use bcrypt::verify; use captcha::{gen, Difficulty}; use chrono::Duration; +use lemmy_apub::ApubObjectType; use lemmy_db::{ comment::*, comment_view::*, diff --git a/src/lib.rs b/src/lib.rs index 4f51a87dd..d8c31f049 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,18 +1,18 @@ #![recursion_limit = "512"] -#[macro_use] -extern crate lazy_static; pub mod api; -pub mod apub; pub mod code_migrations; -pub mod request; pub mod routes; pub mod version; -use crate::request::{retry, RecvError}; use anyhow::anyhow; use lemmy_db::DbPool; -use lemmy_utils::{apub::get_apub_protocol_string, settings::Settings, LemmyError}; +use lemmy_utils::{ + apub::get_apub_protocol_string, + request::{retry, RecvError}, + settings::Settings, + LemmyError, +}; use log::error; use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC}; use reqwest::Client; diff --git a/src/main.rs b/src/main.rs index 818216326..c37a1f30f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,11 +16,11 @@ use diesel::{ PgConnection, }; use lazy_static::lazy_static; +use lemmy_apub::activity_queue::create_activity_queue; use lemmy_db::get_database_url_from_env; use lemmy_rate_limit::{rate_limiter::RateLimiter, RateLimit}; use lemmy_server::{ api::match_websocket_operation, - apub::activity_queue::create_activity_queue, code_migrations::run_advanced_migrations, routes::*, }; diff --git a/src/routes/federation.rs b/src/routes/federation.rs index 2a0c81b23..36cfb8518 100644 --- a/src/routes/federation.rs +++ b/src/routes/federation.rs @@ -1,4 +1,6 @@ -use crate::apub::{ +use actix_web::*; +use http_signature_normalization_actix::digest::middleware::VerifyDigest; +use lemmy_apub::{ comment::get_apub_comment, community::*, inbox::{community_inbox::community_inbox, shared_inbox::shared_inbox, user_inbox::user_inbox}, @@ -6,8 +8,6 @@ use crate::apub::{ user::*, APUB_JSON_CONTENT_TYPE, }; -use actix_web::*; -use http_signature_normalization_actix::digest::middleware::VerifyDigest; use lemmy_utils::settings::Settings; use sha2::{Digest, Sha256}; diff --git a/src/routes/webfinger.rs b/src/routes/webfinger.rs index 40d100262..bef94e6f9 100644 --- a/src/routes/webfinger.rs +++ b/src/routes/webfinger.rs @@ -1,7 +1,7 @@ use actix_web::{error::ErrorBadRequest, web::Query, *}; use anyhow::anyhow; use lemmy_db::{community::Community, user::User_}; -use lemmy_structs::blocking; +use lemmy_structs::{blocking, WebFingerLink, WebFingerResponse}; use lemmy_utils::{ settings::Settings, LemmyError, @@ -9,30 +9,13 @@ use lemmy_utils::{ WEBFINGER_USER_REGEX, }; use lemmy_websocket::LemmyContext; -use serde::{Deserialize, Serialize}; +use serde::Deserialize; #[derive(Deserialize)] pub struct Params { resource: String, } -#[derive(Serialize, Deserialize, Debug)] -pub struct WebFingerResponse { - pub subject: String, - pub aliases: Vec, - pub links: Vec, -} - -#[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, -} - pub fn config(cfg: &mut web::ServiceConfig) { if Settings::get().federation.enabled { cfg.route(