Merge pull request #1210 from LemmyNet/apub-code-docs

Create rustdoc for activitypub code
This commit is contained in:
Dessalines 2020-10-19 11:47:17 -04:00 committed by GitHub
commit 7673e60654
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 174 additions and 82 deletions

View file

@ -19,6 +19,7 @@ use anyhow::Context;
use lemmy_utils::{location_info, LemmyError}; use lemmy_utils::{location_info, LemmyError};
use lemmy_websocket::LemmyContext; use lemmy_websocket::LemmyContext;
/// Takes an announce and passes the inner activity to the appropriate handler.
pub async fn receive_announce( pub async fn receive_announce(
context: &LemmyContext, context: &LemmyContext,
activity: AnyBase, activity: AnyBase,

View file

@ -31,6 +31,7 @@ mod undo_comment;
mod undo_post; mod undo_post;
pub mod update; pub mod update;
/// Return HTTP 501 for unsupported activities in inbox.
fn receive_unhandled_activity<A>(activity: A) -> Result<HttpResponse, LemmyError> fn receive_unhandled_activity<A>(activity: A) -> Result<HttpResponse, LemmyError>
where where
A: Debug, A: Debug,
@ -39,6 +40,8 @@ where
Ok(HttpResponse::NotImplemented().finish()) Ok(HttpResponse::NotImplemented().finish())
} }
/// Reads the destination community from the activity's `cc` field. If this refers to a local
/// community, the activity is announced to all community followers.
async fn announce_if_community_is_local<T, Kind>( async fn announce_if_community_is_local<T, Kind>(
activity: T, activity: T,
user: &User_, user: &User_,
@ -52,16 +55,12 @@ where
{ {
let cc = activity.cc().context(location_info!())?; let cc = activity.cc().context(location_info!())?;
let cc = cc.as_many().context(location_info!())?; let cc = cc.as_many().context(location_info!())?;
let community_followers_uri = cc let community_uri = cc
.first() .first()
.context(location_info!())? .context(location_info!())?
.as_xsd_any_uri() .as_xsd_any_uri()
.context(location_info!())?; .context(location_info!())?;
// TODO: this is hacky but seems to be the only way to get the community ID let community = get_or_fetch_and_upsert_community(&community_uri, context).await?;
let community_uri = community_followers_uri
.to_string()
.replace("/followers", "");
let community = get_or_fetch_and_upsert_community(&Url::parse(&community_uri)?, context).await?;
if community.local { if community.local {
community community
@ -71,6 +70,7 @@ where
Ok(()) Ok(())
} }
/// Reads the actor field of an activity and returns the corresponding `User_`.
pub(crate) async fn get_actor_as_user<T, A>( pub(crate) async fn get_actor_as_user<T, A>(
activity: &T, activity: &T,
context: &LemmyContext, context: &LemmyContext,
@ -89,6 +89,9 @@ pub(crate) enum FindResults {
Post(Post), Post(Post),
} }
/// Tries to find a community, 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_by_id( pub(crate) async fn find_by_id(
context: &LemmyContext, context: &LemmyContext,
apub_id: Url, apub_id: Url,
@ -123,6 +126,11 @@ pub(crate) async fn find_by_id(
return Err(NotFound.into()); return Err(NotFound.into());
} }
/// Ensure that the ID of an incoming activity comes from the same domain as the actor. Optionally
/// also checks the ID of the inner object.
///
/// The reason that this starts with the actor ID is that it was already confirmed as correct by the
/// HTTP signature.
pub(crate) fn verify_activity_domains_valid<T, Kind>( pub(crate) fn verify_activity_domains_valid<T, Kind>(
activity: &T, activity: &T,
actor_id: Url, actor_id: Url,

View file

@ -41,7 +41,8 @@ use url::Url;
#[async_trait::async_trait(?Send)] #[async_trait::async_trait(?Send)]
impl ApubObjectType for Comment { impl ApubObjectType for Comment {
/// Send out information about a newly created comment, to the followers of the community. /// Send out information about a newly created comment, to the followers of the community and
/// mentioned users.
async fn send_create(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError> { async fn send_create(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
let note = self.to_apub(context.pool()).await?; let note = self.to_apub(context.pool()).await?;
@ -68,12 +69,13 @@ impl ApubObjectType for Comment {
// Set the mention tags // Set the mention tags
.set_many_tags(maa.get_tags()?); .set_many_tags(maa.get_tags()?);
send_to_community(&creator, &community, create.clone(), context).await?; send_to_community(create.clone(), &creator, &community, context).await?;
send_comment_mentions(&creator, maa.inboxes, create, context).await?; send_comment_mentions(&creator, maa.inboxes, create, context).await?;
Ok(()) Ok(())
} }
/// Send out information about an edited post, to the followers of the community. /// Send out information about an edited post, to the followers of the community and mentioned
/// users.
async fn send_update(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError> { async fn send_update(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
let note = self.to_apub(context.pool()).await?; let note = self.to_apub(context.pool()).await?;
@ -100,7 +102,7 @@ impl ApubObjectType for Comment {
// Set the mention tags // Set the mention tags
.set_many_tags(maa.get_tags()?); .set_many_tags(maa.get_tags()?);
send_to_community(&creator, &community, update.clone(), context).await?; send_to_community(update.clone(), &creator, &community, context).await?;
send_comment_mentions(&creator, maa.inboxes, update, context).await?; send_comment_mentions(&creator, maa.inboxes, update, context).await?;
Ok(()) Ok(())
} }
@ -122,7 +124,7 @@ impl ApubObjectType for Comment {
.set_to(public()) .set_to(public())
.set_many_ccs(vec![community.actor_id()?]); .set_many_ccs(vec![community.actor_id()?]);
send_to_community(&creator, &community, delete, context).await?; send_to_community(delete, &creator, &community, context).await?;
Ok(()) Ok(())
} }
@ -156,7 +158,7 @@ impl ApubObjectType for Comment {
.set_to(public()) .set_to(public())
.set_many_ccs(vec![community.actor_id()?]); .set_many_ccs(vec![community.actor_id()?]);
send_to_community(&creator, &community, undo, context).await?; send_to_community(undo, &creator, &community, context).await?;
Ok(()) Ok(())
} }
@ -177,7 +179,7 @@ impl ApubObjectType for Comment {
.set_to(public()) .set_to(public())
.set_many_ccs(vec![community.actor_id()?]); .set_many_ccs(vec![community.actor_id()?]);
send_to_community(&mod_, &community, remove, context).await?; send_to_community(remove, &mod_, &community, context).await?;
Ok(()) Ok(())
} }
@ -207,7 +209,7 @@ impl ApubObjectType for Comment {
.set_to(public()) .set_to(public())
.set_many_ccs(vec![community.actor_id()?]); .set_many_ccs(vec![community.actor_id()?]);
send_to_community(&mod_, &community, undo, context).await?; send_to_community(undo, &mod_, &community, context).await?;
Ok(()) Ok(())
} }
} }
@ -233,7 +235,7 @@ impl ApubLikeableType for Comment {
.set_to(public()) .set_to(public())
.set_many_ccs(vec![community.actor_id()?]); .set_many_ccs(vec![community.actor_id()?]);
send_to_community(&creator, &community, like, context).await?; send_to_community(like, &creator, &community, context).await?;
Ok(()) Ok(())
} }
@ -256,7 +258,7 @@ impl ApubLikeableType for Comment {
.set_to(public()) .set_to(public())
.set_many_ccs(vec![community.actor_id()?]); .set_many_ccs(vec![community.actor_id()?]);
send_to_community(&creator, &community, dislike, context).await?; send_to_community(dislike, &creator, &community, context).await?;
Ok(()) Ok(())
} }
@ -291,7 +293,7 @@ impl ApubLikeableType for Comment {
.set_to(public()) .set_to(public())
.set_many_ccs(vec![community.actor_id()?]); .set_many_ccs(vec![community.actor_id()?]);
send_to_community(&creator, &community, undo, context).await?; send_to_community(undo, &creator, &community, context).await?;
Ok(()) Ok(())
} }
} }

View file

@ -84,6 +84,7 @@ impl ActorType for Community {
Ok(()) Ok(())
} }
/// If the creator of a community deletes the community, send this to all followers.
async fn send_delete(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError> { async fn send_delete(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
let group = self.to_apub(context.pool()).await?; let group = self.to_apub(context.pool()).await?;
@ -98,6 +99,7 @@ impl ActorType for Community {
Ok(()) Ok(())
} }
/// If the creator of a community reverts the deletion of a community, send this to all followers.
async fn send_undo_delete( async fn send_undo_delete(
&self, &self,
creator: &User_, creator: &User_,
@ -123,6 +125,7 @@ impl ActorType for Community {
Ok(()) Ok(())
} }
/// If an admin removes a community, send this to all followers.
async fn send_remove(&self, mod_: &User_, context: &LemmyContext) -> Result<(), LemmyError> { async fn send_remove(&self, mod_: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
let mut remove = Remove::new(mod_.actor_id.to_owned(), self.actor_id()?); let mut remove = Remove::new(mod_.actor_id.to_owned(), self.actor_id()?);
remove remove
@ -135,6 +138,7 @@ impl ActorType for Community {
Ok(()) Ok(())
} }
/// If an admin reverts the removal of a community, send this to all followers.
async fn send_undo_remove(&self, mod_: &User_, context: &LemmyContext) -> Result<(), LemmyError> { async fn send_undo_remove(&self, mod_: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
let mut remove = Remove::new(mod_.actor_id.to_owned(), self.actor_id()?); let mut remove = Remove::new(mod_.actor_id.to_owned(), self.actor_id()?);
remove remove
@ -155,6 +159,8 @@ impl ActorType for Community {
Ok(()) Ok(())
} }
/// Wraps an activity sent to the community in an announce, and then sends the announce to all
/// community followers.
async fn send_announce( async fn send_announce(
&self, &self,
activity: AnyBase, activity: AnyBase,

View file

@ -8,6 +8,8 @@ pub mod post;
pub mod private_message; pub mod private_message;
pub mod user; pub mod user;
/// Generate a unique ID for an activity, in the format:
/// `http(s)://example.com/receive/create/202daf0a-1489-45df-8d2e-c8a3173fed36`
fn generate_activity_id<T>(kind: T) -> Result<Url, ParseError> fn generate_activity_id<T>(kind: T) -> Result<Url, ParseError>
where where
T: ToString, T: ToString,

View file

@ -45,7 +45,7 @@ impl ApubObjectType for Post {
.set_to(public()) .set_to(public())
.set_many_ccs(vec![community.actor_id()?]); .set_many_ccs(vec![community.actor_id()?]);
send_to_community(creator, &community, create, context).await?; send_to_community(create, creator, &community, context).await?;
Ok(()) Ok(())
} }
@ -66,7 +66,7 @@ impl ApubObjectType for Post {
.set_to(public()) .set_to(public())
.set_many_ccs(vec![community.actor_id()?]); .set_many_ccs(vec![community.actor_id()?]);
send_to_community(creator, &community, update, context).await?; send_to_community(update, creator, &community, context).await?;
Ok(()) Ok(())
} }
@ -84,7 +84,7 @@ impl ApubObjectType for Post {
.set_to(public()) .set_to(public())
.set_many_ccs(vec![community.actor_id()?]); .set_many_ccs(vec![community.actor_id()?]);
send_to_community(creator, &community, delete, context).await?; send_to_community(delete, creator, &community, context).await?;
Ok(()) Ok(())
} }
@ -114,7 +114,7 @@ impl ApubObjectType for Post {
.set_to(public()) .set_to(public())
.set_many_ccs(vec![community.actor_id()?]); .set_many_ccs(vec![community.actor_id()?]);
send_to_community(creator, &community, undo, context).await?; send_to_community(undo, creator, &community, context).await?;
Ok(()) Ok(())
} }
@ -132,7 +132,7 @@ impl ApubObjectType for Post {
.set_to(public()) .set_to(public())
.set_many_ccs(vec![community.actor_id()?]); .set_many_ccs(vec![community.actor_id()?]);
send_to_community(mod_, &community, remove, context).await?; send_to_community(remove, mod_, &community, context).await?;
Ok(()) Ok(())
} }
@ -158,7 +158,7 @@ impl ApubObjectType for Post {
.set_to(public()) .set_to(public())
.set_many_ccs(vec![community.actor_id()?]); .set_many_ccs(vec![community.actor_id()?]);
send_to_community(mod_, &community, undo, context).await?; send_to_community(undo, mod_, &community, context).await?;
Ok(()) Ok(())
} }
} }
@ -181,7 +181,7 @@ impl ApubLikeableType for Post {
.set_to(public()) .set_to(public())
.set_many_ccs(vec![community.actor_id()?]); .set_many_ccs(vec![community.actor_id()?]);
send_to_community(&creator, &community, like, context).await?; send_to_community(like, &creator, &community, context).await?;
Ok(()) Ok(())
} }
@ -201,7 +201,7 @@ impl ApubLikeableType for Post {
.set_to(public()) .set_to(public())
.set_many_ccs(vec![community.actor_id()?]); .set_many_ccs(vec![community.actor_id()?]);
send_to_community(&creator, &community, dislike, context).await?; send_to_community(dislike, &creator, &community, context).await?;
Ok(()) Ok(())
} }
@ -233,7 +233,7 @@ impl ApubLikeableType for Post {
.set_to(public()) .set_to(public())
.set_many_ccs(vec![community.actor_id()?]); .set_many_ccs(vec![community.actor_id()?]);
send_to_community(&creator, &community, undo, context).await?; send_to_community(undo, &creator, &community, context).await?;
Ok(()) Ok(())
} }
} }

View file

@ -41,7 +41,7 @@ impl ApubObjectType for PrivateMessage {
Ok(()) Ok(())
} }
/// Send out information about an edited post, to the followers of the community. /// Send out information about an edited private message, to the followers of the community.
async fn send_update(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError> { async fn send_update(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
let note = self.to_apub(context.pool()).await?; let note = self.to_apub(context.pool()).await?;

View file

@ -28,6 +28,11 @@ use serde::{export::fmt::Debug, Deserialize, Serialize};
use std::{collections::BTreeMap, future::Future, pin::Pin}; use std::{collections::BTreeMap, future::Future, pin::Pin};
use url::Url; use url::Url;
/// Sends a local activity to a single, remote actor.
///
/// * `activity` the apub activity to be sent
/// * `creator` the local actor which created the activity
/// * `inbox` the inbox url where the activity should be delivered to
pub async fn send_activity_single_dest<T, Kind>( pub async fn send_activity_single_dest<T, Kind>(
activity: T, activity: T,
creator: &dyn ActorType, creator: &dyn ActorType,
@ -59,6 +64,12 @@ where
Ok(()) Ok(())
} }
/// From a local community, send activity to all remote followers.
///
/// * `activity` the apub activity to send
/// * `community` the sending community
/// * `sender_shared_inbox` in case of an announce, this should be the shared inbox of the inner
/// activities creator, as receiving a known activity will cause an error
pub async fn send_to_community_followers<T, Kind>( pub async fn send_to_community_followers<T, Kind>(
activity: T, activity: T,
community: &Community, community: &Community,
@ -102,10 +113,16 @@ where
Ok(()) Ok(())
} }
/// Sends an activity from a local user to a remote community.
///
/// * `activity` the activity to send
/// * `creator` the creator of the activity
/// * `community` the destination community
///
pub async fn send_to_community<T, Kind>( pub async fn send_to_community<T, Kind>(
activity: T,
creator: &User_, creator: &User_,
community: &Community, community: &Community,
activity: T,
context: &LemmyContext, context: &LemmyContext,
) -> Result<(), LemmyError> ) -> Result<(), LemmyError>
where where
@ -140,6 +157,11 @@ where
Ok(()) Ok(())
} }
/// Sends notification to any users mentioned in a comment
///
/// * `creator` user who created the comment
/// * `mentions` list of inboxes of users which are mentioned in the comment
/// * `activity` either a `Create/Note` or `Update/Note`
pub async fn send_comment_mentions<T, Kind>( pub async fn send_comment_mentions<T, Kind>(
creator: &User_, creator: &User_,
mentions: Vec<Url>, mentions: Vec<Url>,
@ -173,7 +195,8 @@ where
Ok(()) Ok(())
} }
/// Asynchronously sends the given `activity` from `actor` to every inbox URL in `to`. /// Create new `SendActivityTasks`, which will deliver the given activity to inboxes, as well as
/// handling signing and retrying failed deliveres.
/// ///
/// The caller of this function needs to remove any blocked domains from `to`, /// The caller of this function needs to remove any blocked domains from `to`,
/// using `check_is_apub_id_valid()`. /// using `check_is_apub_id_valid()`.
@ -224,6 +247,8 @@ struct SendActivityTask {
private_key: String, private_key: String,
} }
/// Signs the activity with the sending actor's key, and delivers to the given inbox. Also retries
/// if the delivery failed.
impl ActixJob for SendActivityTask { impl ActixJob for SendActivityTask {
type State = MyState; type State = MyState;
type Future = Pin<Box<dyn Future<Output = Result<(), Error>>>>; type Future = Pin<Box<dyn Future<Output = Result<(), Error>>>>;

View file

@ -5,6 +5,8 @@ use lemmy_db::{category::Category, Crud};
use lemmy_utils::LemmyError; use lemmy_utils::LemmyError;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
/// Activitystreams extension to allow (de)serializing additional Community fields `category` and
/// `sensitive` (called 'nsfw' in Lemmy).
#[derive(Clone, Debug, Default, Deserialize, Serialize)] #[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct GroupExtension { pub struct GroupExtension {

View file

@ -2,6 +2,9 @@ use activitystreams::unparsed::UnparsedMutExt;
use activitystreams_ext::UnparsedExtension; use activitystreams_ext::UnparsedExtension;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
/// Activitystreams extension to allow (de)serializing additional Post fields
/// `comemnts_enabled` (called 'locked' in Lemmy),
/// `sensitive` (called 'nsfw') and `stickied`.
#[derive(Clone, Debug, Default, Deserialize, Serialize)] #[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct PageExtension { pub struct PageExtension {

View file

@ -24,11 +24,12 @@ lazy_static! {
static ref HTTP_SIG_CONFIG: Config = Config::new(); static ref HTTP_SIG_CONFIG: Config = Config::new();
} }
/// Signs request headers with the given keypair. /// Creates an HTTP post request to `inbox_url`, with the given `client` and `headers`, and
/// `activity` as request body. The request is signed with `private_key` and then sent.
pub async fn sign_and_send( pub async fn sign_and_send(
client: &Client, client: &Client,
headers: BTreeMap<String, String>, headers: BTreeMap<String, String>,
url: &Url, inbox_url: &Url,
activity: String, activity: String,
actor_id: &Url, actor_id: &Url,
private_key: String, private_key: String,
@ -43,7 +44,7 @@ pub async fn sign_and_send(
); );
} }
let response = client let response = client
.post(&url.to_string()) .post(&inbox_url.to_string())
.headers(header_map) .headers(header_map)
.signature_with_digest( .signature_with_digest(
HTTP_SIG_CONFIG.clone(), HTTP_SIG_CONFIG.clone(),
@ -63,6 +64,7 @@ pub async fn sign_and_send(
Ok(response) Ok(response)
} }
/// Verifies the HTTP signature on an incoming inbox request.
pub fn verify_signature(request: &HttpRequest, actor: &dyn ActorType) -> Result<(), LemmyError> { pub fn verify_signature(request: &HttpRequest, actor: &dyn ActorType) -> Result<(), LemmyError> {
let public_key = actor.public_key().context(location_info!())?; let public_key = actor.public_key().context(location_info!())?;
let verified = CONFIG2 let verified = CONFIG2
@ -90,8 +92,14 @@ pub fn verify_signature(request: &HttpRequest, actor: &dyn ActorType) -> Result<
} }
} }
// The following is taken from here: /// Extension for actor public key, which is needed on user and community for HTTP signatures.
// https://docs.rs/activitystreams/0.5.0-alpha.17/activitystreams/ext/index.html ///
/// Taken from: https://docs.rs/activitystreams/0.5.0-alpha.17/activitystreams/ext/index.html
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PublicKeyExtension {
pub public_key: PublicKey,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)] #[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
@ -101,12 +109,6 @@ pub struct PublicKey {
pub public_key_pem: String, pub public_key_pem: String,
} }
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PublicKeyExtension {
pub public_key: PublicKey,
}
impl PublicKey { impl PublicKey {
pub fn to_ext(&self) -> PublicKeyExtension { pub fn to_ext(&self) -> PublicKeyExtension {
PublicKeyExtension { PublicKeyExtension {

View file

@ -82,11 +82,11 @@ pub enum SearchAcceptedObjects {
/// 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:
/// http://lemmy_alpha:8540/c/main, or !main@lemmy_alpha:8540 /// http://lemmy_alpha:8541/c/main, or !main@lemmy_alpha:8541
/// http://lemmy_alpha:8540/u/lemmy_alpha, or @lemmy_alpha@lemmy_alpha:8540 /// http://lemmy_beta:8551/u/lemmy_alpha, or @lemmy_beta@lemmy_beta:8551
/// http://lemmy_alpha:8540/post/3 /// http://lemmy_gamma:8561/post/3
/// http://lemmy_alpha:8540/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,
context: &LemmyContext, context: &LemmyContext,
@ -191,19 +191,27 @@ pub async fn search_by_apub_id(
Ok(response) Ok(response)
} }
/// Get a remote actor from its apub ID (either a user or a community). Thin wrapper around
/// `get_or_fetch_and_upsert_user()` and `get_or_fetch_and_upsert_community()`.
///
/// If it exists locally and `!should_refetch_actor()`, it is returned directly from the database.
/// Otherwise it is fetched from the remote instance, stored and returned.
pub(crate) async fn get_or_fetch_and_upsert_actor( pub(crate) async fn get_or_fetch_and_upsert_actor(
apub_id: &Url, apub_id: &Url,
context: &LemmyContext, context: &LemmyContext,
) -> Result<Box<dyn ActorType>, LemmyError> { ) -> Result<Box<dyn ActorType>, LemmyError> {
let user = get_or_fetch_and_upsert_user(apub_id, context).await; let community = get_or_fetch_and_upsert_community(apub_id, context).await;
let actor: Box<dyn ActorType> = match user { let actor: Box<dyn ActorType> = match community {
Ok(u) => Box::new(u), Ok(c) => Box::new(c),
Err(_) => Box::new(get_or_fetch_and_upsert_community(apub_id, context).await?), Err(_) => Box::new(get_or_fetch_and_upsert_user(apub_id, context).await?),
}; };
Ok(actor) Ok(actor)
} }
/// Check if a remote user exists, create if not found, if its too old update it.Fetch a user, insert/update it in the database and return the user. /// Get a user from its apub ID.
///
/// If it exists locally and `!should_refetch_actor()`, it is returned directly from the database.
/// Otherwise it is fetched from the remote instance, stored and returned.
pub(crate) async fn get_or_fetch_and_upsert_user( pub(crate) async fn get_or_fetch_and_upsert_user(
apub_id: &Url, apub_id: &Url,
context: &LemmyContext, context: &LemmyContext,
@ -241,7 +249,8 @@ pub(crate) async fn get_or_fetch_and_upsert_user(
} }
/// Determines when a remote actor should be refetched from its instance. In release builds, this is /// Determines when a remote actor should be refetched from its instance. In release builds, this is
/// ACTOR_REFETCH_INTERVAL_SECONDS after the last refetch, in debug builds always. /// `ACTOR_REFETCH_INTERVAL_SECONDS` after the last refetch, in debug builds
/// `ACTOR_REFETCH_INTERVAL_SECONDS_DEBUG`.
/// ///
/// TODO it won't pick up new avatars, summaries etc until a day after. /// TODO it won't pick up new avatars, summaries etc until a day after.
/// Actors need an "update" activity pushed to other servers to fix this. /// Actors need an "update" activity pushed to other servers to fix this.
@ -255,7 +264,10 @@ fn should_refetch_actor(last_refreshed: NaiveDateTime) -> bool {
last_refreshed.lt(&(naive_now() - update_interval)) last_refreshed.lt(&(naive_now() - update_interval))
} }
/// Check if a remote community exists, create if not found, if its too old update it.Fetch a community, insert/update it in the database and return the community. /// Get a community from its apub ID.
///
/// If it exists locally and `!should_refetch_actor()`, it is returned directly from the database.
/// Otherwise it is fetched from the remote instance, stored and returned.
pub(crate) async fn get_or_fetch_and_upsert_community( pub(crate) async fn get_or_fetch_and_upsert_community(
apub_id: &Url, apub_id: &Url,
context: &LemmyContext, context: &LemmyContext,
@ -280,6 +292,9 @@ pub(crate) async fn get_or_fetch_and_upsert_community(
} }
} }
/// Request a community by apub ID from a remote instance, including moderators. If `community_id`,
/// is set, this is an update for a community which is already known locally. If not, we don't know
/// the community yet and also pull the outbox, to get some initial posts.
async fn fetch_remote_community( async fn fetch_remote_community(
apub_id: &Url, apub_id: &Url,
context: &LemmyContext, context: &LemmyContext,
@ -358,6 +373,10 @@ async fn fetch_remote_community(
Ok(community) Ok(community)
} }
/// Gets a post by its apub ID. If it exists locally, it is returned directly. Otherwise it is
/// pulled from its apub ID, inserted and returned.
///
/// The parent community is also pulled if necessary. Comments are not pulled.
pub(crate) async fn get_or_fetch_and_insert_post( pub(crate) async fn get_or_fetch_and_insert_post(
post_ap_id: &Url, post_ap_id: &Url,
context: &LemmyContext, context: &LemmyContext,
@ -383,6 +402,10 @@ pub(crate) async fn get_or_fetch_and_insert_post(
} }
} }
/// Gets a comment by its apub ID. If it exists locally, it is returned directly. Otherwise it is
/// pulled from its apub ID, inserted and returned.
///
/// The parent community, post and comment are also pulled if necessary.
pub(crate) async fn get_or_fetch_and_insert_comment( pub(crate) async fn get_or_fetch_and_insert_comment(
comment_ap_id: &Url, comment_ap_id: &Url,
context: &LemmyContext, context: &LemmyContext,

View file

@ -15,7 +15,7 @@ pub struct CommentQuery {
comment_id: String, comment_id: String,
} }
/// Return the post json over HTTP. /// Return the ActivityPub json representation of a local comment over HTTP.
pub async fn get_apub_comment( pub async fn get_apub_comment(
info: Path<CommentQuery>, info: Path<CommentQuery>,
context: web::Data<LemmyContext>, context: web::Data<LemmyContext>,

View file

@ -19,7 +19,7 @@ pub struct CommunityQuery {
community_name: String, community_name: String,
} }
/// Return the community json over HTTP. /// Return the ActivityPub json representation of a local community over HTTP.
pub async fn get_apub_community_http( pub async fn get_apub_community_http(
info: web::Path<CommunityQuery>, info: web::Path<CommunityQuery>,
context: web::Data<LemmyContext>, context: web::Data<LemmyContext>,
@ -62,6 +62,8 @@ pub async fn get_apub_community_followers(
Ok(create_apub_response(&collection)) Ok(create_apub_response(&collection))
} }
/// Returns the community outbox, which is populated by a maximum of 20 posts (but no other
/// activites like votes or comments).
pub async fn get_apub_community_outbox( pub async fn get_apub_community_outbox(
info: web::Path<CommunityQuery>, info: web::Path<CommunityQuery>,
context: web::Data<LemmyContext>, context: web::Data<LemmyContext>,

View file

@ -15,7 +15,7 @@ pub struct PostQuery {
post_id: String, post_id: String,
} }
/// Return the post json over HTTP. /// Return the ActivityPub json representation of a local post over HTTP.
pub async fn get_apub_post( pub async fn get_apub_post(
info: web::Path<PostQuery>, info: web::Path<PostQuery>,
context: web::Data<LemmyContext>, context: web::Data<LemmyContext>,

View file

@ -11,7 +11,7 @@ pub struct UserQuery {
user_name: String, user_name: String,
} }
/// Return the user json over HTTP. /// Return the ActivityPub json representation of a local user over HTTP.
pub async fn get_apub_user_http( pub async fn get_apub_user_http(
info: web::Path<UserQuery>, info: web::Path<UserQuery>,
context: web::Data<LemmyContext>, context: web::Data<LemmyContext>,

View file

@ -25,6 +25,7 @@ use log::info;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::fmt::Debug; use std::fmt::Debug;
/// Allowed activities for community inbox.
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Deserialize, Serialize)] #[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Deserialize, Serialize)]
#[serde(rename_all = "PascalCase")] #[serde(rename_all = "PascalCase")]
pub enum ValidTypes { pub enum ValidTypes {
@ -90,7 +91,7 @@ pub async fn community_inbox(
res res
} }
/// Handle a follow request from a remote user, adding it to the local database and returning an /// Handle a follow request from a remote user, adding the user as follower and returning an
/// Accept activity. /// Accept activity.
async fn handle_follow( async fn handle_follow(
activity: AnyBase, activity: AnyBase,
@ -117,6 +118,7 @@ async fn handle_follow(
Ok(HttpResponse::Ok().finish()) Ok(HttpResponse::Ok().finish())
} }
/// Handle `Undo/Follow` from a user, removing the user from followers list.
async fn handle_undo_follow( async fn handle_undo_follow(
activity: AnyBase, activity: AnyBase,
user: User_, user: User_,

View file

@ -23,6 +23,7 @@ use log::debug;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::fmt::Debug; use std::fmt::Debug;
/// Allowed activity types for shared inbox.
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Deserialize, Serialize)] #[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Deserialize, Serialize)]
#[serde(rename_all = "PascalCase")] #[serde(rename_all = "PascalCase")]
pub enum ValidTypes { pub enum ValidTypes {
@ -40,7 +41,7 @@ pub enum ValidTypes {
// but it might still work due to the anybase conversion // but it might still work due to the anybase conversion
pub type AcceptedActivities = ActorAndObject<ValidTypes>; pub type AcceptedActivities = ActorAndObject<ValidTypes>;
/// Handler for all incoming receive to user inboxes. /// Handler for all incoming requests to shared inbox.
pub async fn shared_inbox( pub async fn shared_inbox(
request: HttpRequest, request: HttpRequest,
input: web::Json<AcceptedActivities>, input: web::Json<AcceptedActivities>,

View file

@ -30,6 +30,7 @@ use log::debug;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::fmt::Debug; use std::fmt::Debug;
/// Allowed activities for user inbox.
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Deserialize, Serialize)] #[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Deserialize, Serialize)]
#[serde(rename_all = "PascalCase")] #[serde(rename_all = "PascalCase")]
pub enum ValidTypes { pub enum ValidTypes {
@ -42,7 +43,7 @@ pub enum ValidTypes {
pub type AcceptedActivities = ActorAndObject<ValidTypes>; pub type AcceptedActivities = ActorAndObject<ValidTypes>;
/// Handler for all incoming receive to user inboxes. /// Handler for all incoming activities to user inboxes.
pub async fn user_inbox( pub async fn user_inbox(
request: HttpRequest, request: HttpRequest,
input: web::Json<AcceptedActivities>, input: web::Json<AcceptedActivities>,

View file

@ -29,13 +29,24 @@ use lemmy_websocket::LemmyContext;
use serde::Serialize; use serde::Serialize;
use url::{ParseError, Url}; use url::{ParseError, Url};
/// Activitystreams type for community
type GroupExt = Ext2<ApActor<Group>, GroupExtension, PublicKeyExtension>; type GroupExt = Ext2<ApActor<Group>, GroupExtension, PublicKeyExtension>;
/// Activitystreams type for user
type PersonExt = Ext1<ApActor<Person>, PublicKeyExtension>; type PersonExt = Ext1<ApActor<Person>, PublicKeyExtension>;
/// Activitystreams type for post
type PageExt = Ext1<Page, PageExtension>; type PageExt = Ext1<Page, PageExtension>;
pub static APUB_JSON_CONTENT_TYPE: &str = "application/activity+json"; pub static APUB_JSON_CONTENT_TYPE: &str = "application/activity+json";
// Checks if the ID has a valid format, correct scheme, and is in the allowed instance list. /// Checks if the ID is allowed for sending or receiving.
///
/// In particular, it checks for:
/// - federation being enabled (if its disabled, only local URLs are allowed)
/// - the correct scheme (either http or https)
/// - URL being in the allowlist (if it is active)
/// - URL not being in the blocklist (if it is active)
///
/// Note that only one of allowlist and blacklist can be enabled, not both.
fn check_is_apub_id_valid(apub_id: &Url) -> Result<(), LemmyError> { fn check_is_apub_id_valid(apub_id: &Url) -> Result<(), LemmyError> {
let settings = Settings::get(); let settings = Settings::get();
let domain = apub_id.domain().context(location_info!())?.to_string(); let domain = apub_id.domain().context(location_info!())?.to_string();
@ -90,10 +101,11 @@ fn check_is_apub_id_valid(apub_id: &Url) -> Result<(), LemmyError> {
} }
} }
/// Trait for converting an object or actor into the respective ActivityPub type.
#[async_trait::async_trait(?Send)] #[async_trait::async_trait(?Send)]
pub trait ToApub { pub trait ToApub {
type Response; type ApubType;
async fn to_apub(&self, pool: &DbPool) -> Result<Self::Response, LemmyError>; async fn to_apub(&self, pool: &DbPool) -> Result<Self::ApubType, LemmyError>;
fn to_tombstone(&self) -> Result<Tombstone, LemmyError>; fn to_tombstone(&self) -> Result<Tombstone, LemmyError>;
} }
@ -104,9 +116,8 @@ pub trait FromApub {
/// ///
/// * `apub` The object to read from /// * `apub` The object to read from
/// * `context` LemmyContext which holds DB pool, HTTP client etc /// * `context` LemmyContext which holds DB pool, HTTP client etc
/// * `expected_domain` If present, ensure that the apub object comes from the same domain as /// * `expected_domain` If present, ensure that the domains of this and of the apub object ID are
/// this URL /// identical
///
async fn from_apub( async fn from_apub(
apub: &Self::ApubType, apub: &Self::ApubType,
context: &LemmyContext, context: &LemmyContext,
@ -116,6 +127,8 @@ pub trait FromApub {
Self: Sized; Self: Sized;
} }
/// Common functions for ActivityPub objects, which are implemented by most (but not all) objects
/// and actors in Lemmy.
#[async_trait::async_trait(?Send)] #[async_trait::async_trait(?Send)]
pub trait ApubObjectType { pub trait ApubObjectType {
async fn send_create(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError>; async fn send_create(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError>;
@ -138,6 +151,8 @@ pub trait ApubLikeableType {
-> Result<(), LemmyError>; -> Result<(), LemmyError>;
} }
/// Common methods provided by ActivityPub actors (community and user). Not all methods are
/// implemented by all actors.
#[async_trait::async_trait(?Send)] #[async_trait::async_trait(?Send)]
pub trait ActorType { pub trait ActorType {
fn actor_id_str(&self) -> String; fn actor_id_str(&self) -> String;
@ -149,9 +164,6 @@ pub trait ActorType {
/// numeric id in the database, used for insert_activity /// numeric id in the database, used for insert_activity
fn user_id(&self) -> i32; 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( async fn send_follow(
&self, &self,
follow_actor_id: &Url, follow_actor_id: &Url,
@ -163,7 +175,6 @@ pub trait ActorType {
context: &LemmyContext, context: &LemmyContext,
) -> Result<(), LemmyError>; ) -> Result<(), LemmyError>;
#[allow(unused_variables)]
async fn send_accept_follow( async fn send_accept_follow(
&self, &self,
follow: Follow, follow: Follow,
@ -234,6 +245,8 @@ pub trait ActorType {
} }
} }
/// Store a sent or received activity in the database, for logging purposes. These records are not
/// persistent.
pub async fn insert_activity<T>( pub async fn insert_activity<T>(
user_id: i32, user_id: i32,
data: T, data: T,

View file

@ -32,7 +32,7 @@ use url::Url;
#[async_trait::async_trait(?Send)] #[async_trait::async_trait(?Send)]
impl ToApub for Comment { impl ToApub for Comment {
type Response = Note; type ApubType = Note;
async fn to_apub(&self, pool: &DbPool) -> Result<Note, LemmyError> { async fn to_apub(&self, pool: &DbPool) -> Result<Note, LemmyError> {
let mut comment = Note::new(); let mut comment = Note::new();
@ -82,7 +82,9 @@ impl ToApub for Comment {
impl FromApub for CommentForm { impl FromApub for CommentForm {
type ApubType = Note; type ApubType = Note;
/// Parse an ActivityPub note received from another instance into a Lemmy comment /// Converts a `Note` to `CommentForm`.
///
/// If the parent community, post and comment(s) are not known locally, these are also fetched.
async fn from_apub( async fn from_apub(
note: &Note, note: &Note,
context: &LemmyContext, context: &LemmyContext,

View file

@ -32,9 +32,8 @@ use url::Url;
#[async_trait::async_trait(?Send)] #[async_trait::async_trait(?Send)]
impl ToApub for Community { impl ToApub for Community {
type Response = GroupExt; type ApubType = GroupExt;
// Turn a Lemmy Community into an ActivityPub group that can be sent out over the network.
async fn to_apub(&self, pool: &DbPool) -> Result<GroupExt, LemmyError> { async fn to_apub(&self, pool: &DbPool) -> Result<GroupExt, LemmyError> {
// The attributed to, is an ordered vector with the creator actor_ids first, // The attributed to, is an ordered vector with the creator actor_ids first,
// then the rest of the moderators // then the rest of the moderators
@ -108,7 +107,6 @@ impl ToApub for Community {
impl FromApub for CommunityForm { impl FromApub for CommunityForm {
type ApubType = GroupExt; type ApubType = GroupExt;
/// Parse an ActivityPub group received from another instance into a Lemmy community.
async fn from_apub( async fn from_apub(
group: &GroupExt, group: &GroupExt,
context: &LemmyContext, context: &LemmyContext,

View file

@ -31,7 +31,7 @@ use url::Url;
#[async_trait::async_trait(?Send)] #[async_trait::async_trait(?Send)]
impl ToApub for Post { impl ToApub for Post {
type Response = PageExt; type ApubType = PageExt;
// Turn a Lemmy post into an ActivityPub page that can be sent out over the network. // Turn a Lemmy post into an ActivityPub page that can be sent out over the network.
async fn to_apub(&self, pool: &DbPool) -> Result<PageExt, LemmyError> { async fn to_apub(&self, pool: &DbPool) -> Result<PageExt, LemmyError> {
@ -94,7 +94,9 @@ impl ToApub for Post {
impl FromApub for PostForm { impl FromApub for PostForm {
type ApubType = PageExt; type ApubType = PageExt;
/// Parse an ActivityPub page received from another instance into a Lemmy post. /// Converts a `PageExt` to `PostForm`.
///
/// If the post's community or creator are not known locally, these are also fetched.
async fn from_apub( async fn from_apub(
page: &PageExt, page: &PageExt,
context: &LemmyContext, context: &LemmyContext,

View file

@ -23,7 +23,7 @@ use url::Url;
#[async_trait::async_trait(?Send)] #[async_trait::async_trait(?Send)]
impl ToApub for PrivateMessage { impl ToApub for PrivateMessage {
type Response = Note; type ApubType = Note;
async fn to_apub(&self, pool: &DbPool) -> Result<Note, LemmyError> { async fn to_apub(&self, pool: &DbPool) -> Result<Note, LemmyError> {
let mut private_message = Note::new(); let mut private_message = Note::new();
@ -58,7 +58,6 @@ impl ToApub for PrivateMessage {
impl FromApub for PrivateMessageForm { impl FromApub for PrivateMessageForm {
type ApubType = Note; type ApubType = Note;
/// Parse an ActivityPub note received from another instance into a Lemmy Private message
async fn from_apub( async fn from_apub(
note: &Note, note: &Note,
context: &LemmyContext, context: &LemmyContext,

View file

@ -21,11 +21,9 @@ use url::Url;
#[async_trait::async_trait(?Send)] #[async_trait::async_trait(?Send)]
impl ToApub for User_ { impl ToApub for User_ {
type Response = PersonExt; type ApubType = PersonExt;
// Turn a Lemmy Community into an ActivityPub group that can be sent out over the network.
async fn to_apub(&self, _pool: &DbPool) -> Result<PersonExt, LemmyError> { async fn to_apub(&self, _pool: &DbPool) -> Result<PersonExt, LemmyError> {
// TODO go through all these to_string and to_owned()
let mut person = Person::new(); let mut person = Person::new();
person person
.set_context(activitystreams::context()) .set_context(activitystreams::context())
@ -73,7 +71,7 @@ impl ToApub for User_ {
#[async_trait::async_trait(?Send)] #[async_trait::async_trait(?Send)]
impl FromApub for UserForm { impl FromApub for UserForm {
type ApubType = PersonExt; type ApubType = PersonExt;
/// Parse an ActivityPub person received from another instance into a Lemmy user.
async fn from_apub( async fn from_apub(
person: &PersonExt, person: &PersonExt,
_context: &LemmyContext, _context: &LemmyContext,