Added documentation for most functions

pull/722/head
Felix 2020-04-17 17:33:55 +02:00
parent 8908c8b184
commit c5ced6fa5e
9 changed files with 62 additions and 53 deletions

View File

@ -30,6 +30,7 @@ fn populate_object_props(
Ok(()) Ok(())
} }
/// Send an activity to a list of recipients, using the correct headers etc.
fn send_activity<A>(activity: &A, to: Vec<String>) -> Result<(), Error> fn send_activity<A>(activity: &A, to: Vec<String>) -> Result<(), Error>
where where
A: Serialize + Debug, A: Serialize + Debug,
@ -47,7 +48,8 @@ where
Ok(()) Ok(())
} }
fn get_followers(conn: &PgConnection, community: &Community) -> Result<Vec<String>, Error> { /// For a given community, returns the inboxes of all followers.
fn get_follower_inboxes(conn: &PgConnection, community: &Community) -> Result<Vec<String>, Error> {
Ok( Ok(
CommunityFollowerView::for_community(conn, community.id)? CommunityFollowerView::for_community(conn, community.id)?
.iter() .iter()
@ -57,6 +59,7 @@ fn get_followers(conn: &PgConnection, community: &Community) -> Result<Vec<Strin
) )
} }
/// Send out information about a newly created post, to the followers of the community.
pub fn post_create(post: &Post, creator: &User_, conn: &PgConnection) -> Result<(), Error> { pub fn post_create(post: &Post, creator: &User_, conn: &PgConnection) -> Result<(), Error> {
let page = post.as_page(conn)?; let page = post.as_page(conn)?;
let community = Community::read(conn, post.community_id)?; let community = Community::read(conn, post.community_id)?;
@ -70,10 +73,11 @@ pub fn post_create(post: &Post, creator: &User_, conn: &PgConnection) -> Result<
.create_props .create_props
.set_actor_xsd_any_uri(creator.actor_id.to_owned())? .set_actor_xsd_any_uri(creator.actor_id.to_owned())?
.set_object_base_box(page)?; .set_object_base_box(page)?;
send_activity(&create, get_followers(conn, &community)?)?; send_activity(&create, get_follower_inboxes(conn, &community)?)?;
Ok(()) Ok(())
} }
/// Send out information about an edited post, to the followers of the community.
pub fn post_update(post: &Post, creator: &User_, conn: &PgConnection) -> Result<(), Error> { pub fn post_update(post: &Post, creator: &User_, conn: &PgConnection) -> Result<(), Error> {
let page = post.as_page(conn)?; let page = post.as_page(conn)?;
let community = Community::read(conn, post.community_id)?; let community = Community::read(conn, post.community_id)?;
@ -87,10 +91,11 @@ pub fn post_update(post: &Post, creator: &User_, conn: &PgConnection) -> Result<
.update_props .update_props
.set_actor_xsd_any_uri(creator.actor_id.to_owned())? .set_actor_xsd_any_uri(creator.actor_id.to_owned())?
.set_object_base_box(page)?; .set_object_base_box(page)?;
send_activity(&update, get_followers(conn, &community)?)?; send_activity(&update, get_follower_inboxes(conn, &community)?)?;
Ok(()) Ok(())
} }
/// As a given local user, send out a follow request to a remote community.
pub fn follow_community( pub fn follow_community(
community: &Community, community: &Community,
user: &User_, user: &User_,
@ -111,6 +116,7 @@ pub fn follow_community(
Ok(()) Ok(())
} }
/// As a local community, accept the follow request from a remote user.
pub fn accept_follow(follow: &Follow) -> Result<(), Error> { pub fn accept_follow(follow: &Follow) -> Result<(), Error> {
let mut accept = Accept::new(); let mut accept = Accept::new();
accept accept

View File

@ -7,7 +7,6 @@ use crate::db::establish_unpooled_connection;
use crate::db::post::Post; use crate::db::post::Post;
use crate::db::user::User_; use crate::db::user::User_;
use crate::db::Crud; use crate::db::Crud;
use crate::settings::Settings;
use crate::{convert_datetime, naive_now}; use crate::{convert_datetime, naive_now};
use activitystreams::actor::properties::ApActorProperties; use activitystreams::actor::properties::ApActorProperties;
use activitystreams::collection::OrderedCollection; use activitystreams::collection::OrderedCollection;
@ -30,30 +29,8 @@ pub struct CommunityQuery {
community_name: String, community_name: String,
} }
pub async fn get_apub_community_list(
db: web::Data<Pool<ConnectionManager<PgConnection>>>,
) -> Result<HttpResponse<Body>, Error> {
// TODO: implement pagination
let communities = Community::list_local(&db.get().unwrap())?
.iter()
.map(|c| c.as_group(&db.get().unwrap()))
.collect::<Result<Vec<GroupExt>, Error>>()?;
let mut collection = UnorderedCollection::default();
let oprops: &mut ObjectProperties = collection.as_mut();
oprops.set_context_xsd_any_uri(context())?.set_id(format!(
"{}://{}/federation/communities",
get_apub_protocol_string(),
Settings::get().hostname
))?;
collection
.collection_props
.set_total_items(communities.len() as u64)?
.set_many_items_base_boxes(communities)?;
Ok(create_apub_response(&collection))
}
impl Community { impl Community {
// Turn a Lemmy Community into an ActivityPub group that can be sent out over the network.
fn as_group(&self, conn: &PgConnection) -> Result<GroupExt, Error> { fn as_group(&self, conn: &PgConnection) -> Result<GroupExt, Error> {
let mut group = Group::default(); let mut group = Group::default();
let oprops: &mut ObjectProperties = group.as_mut(); let oprops: &mut ObjectProperties = group.as_mut();
@ -104,6 +81,7 @@ impl Community {
} }
impl CommunityForm { impl CommunityForm {
/// Parse an ActivityPub group received from another instance into a Lemmy community.
pub fn from_group(group: &GroupExt, conn: &PgConnection) -> Result<Self, Error> { pub fn from_group(group: &GroupExt, conn: &PgConnection) -> Result<Self, Error> {
let oprops = &group.base.base.object_props; let oprops = &group.base.base.object_props;
let aprops = &group.base.extension; let aprops = &group.base.extension;
@ -142,6 +120,7 @@ impl CommunityForm {
} }
} }
/// Return the community json over HTTP.
pub async fn get_apub_community_http( pub async fn get_apub_community_http(
info: Path<CommunityQuery>, info: Path<CommunityQuery>,
db: web::Data<Pool<ConnectionManager<PgConnection>>>, db: web::Data<Pool<ConnectionManager<PgConnection>>>,
@ -151,6 +130,7 @@ pub async fn get_apub_community_http(
Ok(create_apub_response(&c)) Ok(create_apub_response(&c))
} }
/// Returns an empty followers collection, only populating the siz (for privacy).
pub async fn get_apub_community_followers( pub async fn get_apub_community_followers(
info: Path<CommunityQuery>, info: Path<CommunityQuery>,
db: web::Data<Pool<ConnectionManager<PgConnection>>>, db: web::Data<Pool<ConnectionManager<PgConnection>>>,
@ -173,6 +153,7 @@ pub async fn get_apub_community_followers(
Ok(create_apub_response(&collection)) Ok(create_apub_response(&collection))
} }
/// Returns an UnorderedCollection with the latest posts from the community.
pub async fn get_apub_community_outbox( pub async fn get_apub_community_outbox(
info: Path<CommunityQuery>, info: Path<CommunityQuery>,
db: web::Data<Pool<ConnectionManager<PgConnection>>>, db: web::Data<Pool<ConnectionManager<PgConnection>>>,

View File

@ -22,6 +22,7 @@ pub struct Params {
community_name: String, community_name: String,
} }
/// Handler for all incoming activities to community inboxes.
pub async fn community_inbox( pub async fn community_inbox(
input: web::Json<CommunityAcceptedObjects>, input: web::Json<CommunityAcceptedObjects>,
params: web::Query<Params>, params: web::Query<Params>,
@ -38,6 +39,8 @@ pub async fn community_inbox(
} }
} }
/// Handle a follow request from a remote user, adding it to the local database and returning an
/// Accept activity.
fn handle_follow(follow: &Follow, conn: &PgConnection) -> Result<HttpResponse, Error> { fn handle_follow(follow: &Follow, conn: &PgConnection) -> Result<HttpResponse, Error> {
// TODO: make sure this is a local community // TODO: make sure this is a local community
let community_uri = follow let community_uri = follow

View File

@ -20,6 +20,7 @@ use serde::Deserialize;
use std::time::Duration; use std::time::Duration;
use url::Url; use url::Url;
// Fetch nodeinfo metadata from a remote instance.
fn _fetch_node_info(domain: &str) -> Result<NodeInfo, Error> { fn _fetch_node_info(domain: &str) -> Result<NodeInfo, Error> {
let well_known_uri = Url::parse(&format!( let well_known_uri = Url::parse(&format!(
"{}://{}/.well-known/nodeinfo", "{}://{}/.well-known/nodeinfo",
@ -60,7 +61,9 @@ fn upsert_post(post_form: &PostForm, conn: &PgConnection) -> Result<Post, Error>
} }
} }
// TODO: add an optional param last_updated and only fetch if its too old /// Fetch any type of ActivityPub object, handling things like HTTP headers, deserialisation,
/// timeouts etc.
/// TODO: add an optional param last_updated and only fetch if its too old
pub fn fetch_remote_object<Response>(url: &Url) -> Result<Response, Error> pub fn fetch_remote_object<Response>(url: &Url) -> Result<Response, Error>
where where
Response: for<'de> Deserialize<'de>, Response: for<'de> Deserialize<'de>,
@ -81,6 +84,7 @@ where
Ok(res) Ok(res)
} }
/// The types of ActivityPub objects that can be fetched directly by searching for their ID.
#[serde(untagged)] #[serde(untagged)]
#[derive(serde::Deserialize)] #[derive(serde::Deserialize)]
pub enum SearchAcceptedObjects { pub enum SearchAcceptedObjects {
@ -89,6 +93,12 @@ pub enum SearchAcceptedObjects {
Page(Box<Page>), Page(Box<Page>),
} }
/// Attempt to parse the query as URL, and fetch an ActivityPub object from it.
///
/// Some working examples for use with the docker/federation/ setup:
/// http://lemmy_alpha:8540/federation/c/main
/// http://lemmy_alpha:8540/federation/u/lemmy_alpha
/// http://lemmy_alpha:8540/federation/p/3
pub fn search_by_apub_id(query: &str, conn: &PgConnection) -> Result<SearchResponse, Error> { pub fn search_by_apub_id(query: &str, conn: &PgConnection) -> Result<SearchResponse, Error> {
let query_url = Url::parse(&query)?; let query_url = Url::parse(&query)?;
let mut response = SearchResponse { let mut response = SearchResponse {
@ -98,10 +108,6 @@ pub fn search_by_apub_id(query: &str, conn: &PgConnection) -> Result<SearchRespo
communities: vec![], communities: vec![],
users: vec![], users: vec![],
}; };
// test with:
// http://lemmy_alpha:8540/federation/c/main
// http://lemmy_alpha:8540/federation/u/lemmy_alpha
// http://lemmy_alpha:8540/federation/p/3
match fetch_remote_object::<SearchAcceptedObjects>(&query_url)? { match fetch_remote_object::<SearchAcceptedObjects>(&query_url)? {
SearchAcceptedObjects::Person(p) => { SearchAcceptedObjects::Person(p) => {
let u = upsert_user(&UserForm::from_person(&p)?, conn)?; let u = upsert_user(&UserForm::from_person(&p)?, conn)?;
@ -120,6 +126,7 @@ pub fn search_by_apub_id(query: &str, conn: &PgConnection) -> Result<SearchRespo
Ok(response) Ok(response)
} }
/// Fetch all posts in the outbox of the given user, and insert them into the database.
fn fetch_community_outbox(community: &Community, conn: &PgConnection) -> Result<Vec<Post>, Error> { fn fetch_community_outbox(community: &Community, conn: &PgConnection) -> Result<Vec<Post>, Error> {
let outbox_url = Url::parse(&community.get_outbox_url())?; let outbox_url = Url::parse(&community.get_outbox_url())?;
let outbox = fetch_remote_object::<OrderedCollection>(&outbox_url)?; let outbox = fetch_remote_object::<OrderedCollection>(&outbox_url)?;
@ -137,12 +144,14 @@ fn fetch_community_outbox(community: &Community, conn: &PgConnection) -> Result<
) )
} }
/// Fetch a user, insert/update it in the database and return the user.
pub fn fetch_remote_user(apub_id: &Url, conn: &PgConnection) -> Result<User_, Error> { pub fn fetch_remote_user(apub_id: &Url, conn: &PgConnection) -> Result<User_, Error> {
let person = fetch_remote_object::<PersonExt>(apub_id)?; let person = fetch_remote_object::<PersonExt>(apub_id)?;
let uf = UserForm::from_person(&person)?; let uf = UserForm::from_person(&person)?;
upsert_user(&uf, conn) upsert_user(&uf, conn)
} }
/// Fetch a community, insert/update it in the database and return the community.
pub fn fetch_remote_community(apub_id: &Url, conn: &PgConnection) -> Result<Community, Error> { pub fn fetch_remote_community(apub_id: &Url, conn: &PgConnection) -> Result<Community, Error> {
let group = fetch_remote_object::<GroupExt>(apub_id)?; let group = fetch_remote_object::<GroupExt>(apub_id)?;
let cf = CommunityForm::from_group(&group, conn)?; let cf = CommunityForm::from_group(&group, conn)?;

View File

@ -13,6 +13,7 @@ use activitystreams::ext::Ext;
use actix_web::body::Body; use actix_web::body::Body;
use actix_web::HttpResponse; use actix_web::HttpResponse;
use openssl::{pkey::PKey, rsa::Rsa}; use openssl::{pkey::PKey, rsa::Rsa};
use serde::ser::Serialize;
use url::Url; use url::Url;
type GroupExt = Ext<Ext<Group, ApActorProperties>, PublicKeyExtension>; type GroupExt = Ext<Ext<Group, ApActorProperties>, PublicKeyExtension>;
@ -27,18 +28,22 @@ pub enum EndpointType {
Comment, Comment,
} }
fn create_apub_response<T>(json: &T) -> HttpResponse<Body> /// Convert the data to json and turn it into an HTTP Response with the correct ActivityPub
/// headers.
fn create_apub_response<T>(data: &T) -> HttpResponse<Body>
where where
T: serde::ser::Serialize, T: Serialize,
{ {
HttpResponse::Ok() HttpResponse::Ok()
.content_type(APUB_JSON_CONTENT_TYPE) .content_type(APUB_JSON_CONTENT_TYPE)
.json(json) .json(data)
} }
// TODO: we will probably need to change apub endpoint urls so that html and activity+json content /// Generates the ActivityPub ID for a given object type and name.
// types are handled at the same endpoint, so that you can copy the url into mastodon search ///
// and have it fetch the object. /// TODO: we will probably need to change apub endpoint urls so that html and activity+json content
/// types are handled at the same endpoint, so that you can copy the url into mastodon search
/// and have it fetch the object.
pub fn make_apub_endpoint(endpoint_type: EndpointType, name: &str) -> Url { pub fn make_apub_endpoint(endpoint_type: EndpointType, name: &str) -> Url {
let point = match endpoint_type { let point = match endpoint_type {
EndpointType::Community => "c", EndpointType::Community => "c",
@ -67,21 +72,16 @@ pub fn get_apub_protocol_string() -> &'static str {
} }
} }
pub fn gen_keypair() -> (Vec<u8>, Vec<u8>) { /// Generate the asymmetric keypair for ActivityPub HTTP signatures.
pub fn gen_keypair_str() -> (String, String) {
let rsa = Rsa::generate(2048).expect("sign::gen_keypair: key generation error"); let rsa = Rsa::generate(2048).expect("sign::gen_keypair: key generation error");
let pkey = PKey::from_rsa(rsa).expect("sign::gen_keypair: parsing error"); let pkey = PKey::from_rsa(rsa).expect("sign::gen_keypair: parsing error");
( let public_key = pkey
pkey .public_key_to_pem()
.public_key_to_pem() .expect("sign::gen_keypair: public key encoding error");
.expect("sign::gen_keypair: public key encoding error"), let private_key = pkey
pkey .private_key_to_pem_pkcs8()
.private_key_to_pem_pkcs8() .expect("sign::gen_keypair: private key encoding error");
.expect("sign::gen_keypair: private key encoding error"),
)
}
pub fn gen_keypair_str() -> (String, String) {
let (public_key, private_key) = gen_keypair();
(vec_bytes_to_str(public_key), vec_bytes_to_str(private_key)) (vec_bytes_to_str(public_key), vec_bytes_to_str(private_key))
} }

View File

@ -20,6 +20,7 @@ pub struct PostQuery {
post_id: String, post_id: String,
} }
/// Return the post json over HTTP.
pub async fn get_apub_post( pub async fn get_apub_post(
info: Path<PostQuery>, info: Path<PostQuery>,
db: web::Data<Pool<ConnectionManager<PgConnection>>>, db: web::Data<Pool<ConnectionManager<PgConnection>>>,
@ -30,6 +31,7 @@ pub async fn get_apub_post(
} }
impl Post { impl Post {
// Turn a Lemmy post into an ActivityPub page that can be sent out over the network.
pub fn as_page(&self, conn: &PgConnection) -> Result<Page, Error> { pub fn as_page(&self, conn: &PgConnection) -> Result<Page, Error> {
let mut page = Page::default(); let mut page = Page::default();
let oprops: &mut ObjectProperties = page.as_mut(); let oprops: &mut ObjectProperties = page.as_mut();
@ -67,6 +69,7 @@ impl Post {
} }
impl PostForm { impl PostForm {
/// Parse an ActivityPub page received from another instance into a Lemmy post.
pub fn from_page(page: &Page, conn: &PgConnection) -> Result<PostForm, Error> { pub fn from_page(page: &Page, conn: &PgConnection) -> Result<PostForm, Error> {
let oprops = &page.object_props; let oprops = &page.object_props;
let creator_id = Url::parse(&oprops.get_attributed_to_xsd_any_uri().unwrap().to_string())?; let creator_id = Url::parse(&oprops.get_attributed_to_xsd_any_uri().unwrap().to_string())?;

View File

@ -1,11 +1,12 @@
// For this example, we'll use the Extensible trait, the Extension trait, the Actor trait, and // For this example, we'll use the Extensible trait, the Extension trait, the Actor trait, and
// the Person type // the Person type
use activitystreams::{actor::Actor, ext::Extension}; use activitystreams::{actor::Actor, ext::Extension};
use serde::{Deserialize, Serialize};
// The following is taken from here: // The following is taken from here:
// https://docs.rs/activitystreams/0.5.0-alpha.17/activitystreams/ext/index.html // https://docs.rs/activitystreams/0.5.0-alpha.17/activitystreams/ext/index.html
#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)] #[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct PublicKey { pub struct PublicKey {
pub id: String, pub id: String,
@ -13,7 +14,7 @@ pub struct PublicKey {
pub public_key_pem: String, pub public_key_pem: String,
} }
#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)] #[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct PublicKeyExtension { pub struct PublicKeyExtension {
pub public_key: PublicKey, pub public_key: PublicKey,

View File

@ -22,6 +22,7 @@ pub struct UserQuery {
user_name: String, user_name: String,
} }
// Turn a Lemmy user into an ActivityPub person and return it as json.
pub async fn get_apub_user( pub async fn get_apub_user(
info: Path<UserQuery>, info: Path<UserQuery>,
db: web::Data<Pool<ConnectionManager<PgConnection>>>, db: web::Data<Pool<ConnectionManager<PgConnection>>>,
@ -64,6 +65,7 @@ pub async fn get_apub_user(
} }
impl UserForm { impl UserForm {
/// Parse an ActivityPub person received from another instance into a Lemmy user.
pub fn from_person(person: &PersonExt) -> Result<Self, Error> { pub fn from_person(person: &PersonExt) -> Result<Self, Error> {
let oprops = &person.base.base.object_props; let oprops = &person.base.base.object_props;
let aprops = &person.base.extension; let aprops = &person.base.extension;

View File

@ -22,6 +22,7 @@ pub struct Params {
user_name: String, user_name: String,
} }
/// Handler for all incoming activities to user inboxes.
pub async fn user_inbox( pub async fn user_inbox(
input: web::Json<UserAcceptedObjects>, input: web::Json<UserAcceptedObjects>,
params: web::Query<Params>, params: web::Query<Params>,
@ -38,6 +39,7 @@ pub async fn user_inbox(
} }
} }
/// Handle create activities and insert them in the database.
fn handle_create(create: &Create, conn: &PgConnection) -> Result<HttpResponse, Error> { fn handle_create(create: &Create, conn: &PgConnection) -> Result<HttpResponse, Error> {
let page = create let page = create
.create_props .create_props
@ -52,6 +54,7 @@ fn handle_create(create: &Create, conn: &PgConnection) -> Result<HttpResponse, E
Ok(HttpResponse::Ok().finish()) Ok(HttpResponse::Ok().finish())
} }
/// Handle update activities and insert them in the database.
fn handle_update(update: &Update, conn: &PgConnection) -> Result<HttpResponse, Error> { fn handle_update(update: &Update, conn: &PgConnection) -> Result<HttpResponse, Error> {
let page = update let page = update
.update_props .update_props
@ -67,6 +70,7 @@ fn handle_update(update: &Update, conn: &PgConnection) -> Result<HttpResponse, E
Ok(HttpResponse::Ok().finish()) Ok(HttpResponse::Ok().finish())
} }
/// Handle accepted follows.
fn handle_accept(_accept: &Accept, _conn: &PgConnection) -> Result<HttpResponse, Error> { fn handle_accept(_accept: &Accept, _conn: &PgConnection) -> Result<HttpResponse, Error> {
// TODO: make sure that we actually requested a follow // TODO: make sure that we actually requested a follow
// TODO: at this point, indicate to the user that they are following the community // TODO: at this point, indicate to the user that they are following the community