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>
where
A: Serialize + Debug,
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(
CommunityFollowerView::for_community(conn, community.id)?
.iter()
)
}
+/// 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> {
let page = post.as_page(conn)?;
let community = Community::read(conn, post.community_id)?;
.create_props
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
.set_object_base_box(page)?;
- send_activity(&create, get_followers(conn, &community)?)?;
+ send_activity(&create, get_follower_inboxes(conn, &community)?)?;
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> {
let page = post.as_page(conn)?;
let community = Community::read(conn, post.community_id)?;
.update_props
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
.set_object_base_box(page)?;
- send_activity(&update, get_followers(conn, &community)?)?;
+ send_activity(&update, get_follower_inboxes(conn, &community)?)?;
Ok(())
}
+/// As a given local user, send out a follow request to a remote community.
pub fn follow_community(
community: &Community,
user: &User_,
Ok(())
}
+/// As a local community, accept the follow request from a remote user.
pub fn accept_follow(follow: &Follow) -> Result<(), Error> {
let mut accept = Accept::new();
accept
use crate::db::post::Post;
use crate::db::user::User_;
use crate::db::Crud;
-use crate::settings::Settings;
use crate::{convert_datetime, naive_now};
use activitystreams::actor::properties::ApActorProperties;
use activitystreams::collection::OrderedCollection;
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 {
+ // 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> {
let mut group = Group::default();
let oprops: &mut ObjectProperties = group.as_mut();
}
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> {
let oprops = &group.base.base.object_props;
let aprops = &group.base.extension;
}
}
+/// Return the community json over HTTP.
pub async fn get_apub_community_http(
info: Path<CommunityQuery>,
db: web::Data<Pool<ConnectionManager<PgConnection>>>,
Ok(create_apub_response(&c))
}
+/// Returns an empty followers collection, only populating the siz (for privacy).
pub async fn get_apub_community_followers(
info: Path<CommunityQuery>,
db: web::Data<Pool<ConnectionManager<PgConnection>>>,
Ok(create_apub_response(&collection))
}
+/// Returns an UnorderedCollection with the latest posts from the community.
pub async fn get_apub_community_outbox(
info: Path<CommunityQuery>,
db: web::Data<Pool<ConnectionManager<PgConnection>>>,
community_name: String,
}
+/// Handler for all incoming activities to community inboxes.
pub async fn community_inbox(
input: web::Json<CommunityAcceptedObjects>,
params: web::Query<Params>,
}
}
+/// 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> {
// TODO: make sure this is a local community
let community_uri = follow
use std::time::Duration;
use url::Url;
+// Fetch nodeinfo metadata from a remote instance.
fn _fetch_node_info(domain: &str) -> Result<NodeInfo, Error> {
let well_known_uri = Url::parse(&format!(
"{}://{}/.well-known/nodeinfo",
}
}
-// 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>
where
Response: for<'de> Deserialize<'de>,
Ok(res)
}
+/// The types of ActivityPub objects that can be fetched directly by searching for their ID.
#[serde(untagged)]
#[derive(serde::Deserialize)]
pub enum SearchAcceptedObjects {
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> {
let query_url = Url::parse(&query)?;
let mut response = SearchResponse {
communities: 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)? {
SearchAcceptedObjects::Person(p) => {
let u = upsert_user(&UserForm::from_person(&p)?, conn)?;
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> {
let outbox_url = Url::parse(&community.get_outbox_url())?;
let outbox = fetch_remote_object::<OrderedCollection>(&outbox_url)?;
)
}
+/// 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> {
let person = fetch_remote_object::<PersonExt>(apub_id)?;
let uf = UserForm::from_person(&person)?;
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> {
let group = fetch_remote_object::<GroupExt>(apub_id)?;
let cf = CommunityForm::from_group(&group, conn)?;
use actix_web::body::Body;
use actix_web::HttpResponse;
use openssl::{pkey::PKey, rsa::Rsa};
+use serde::ser::Serialize;
use url::Url;
type GroupExt = Ext<Ext<Group, ApActorProperties>, PublicKeyExtension>;
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
- T: serde::ser::Serialize,
+ T: Serialize,
{
HttpResponse::Ok()
.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
-// types are handled at the same endpoint, so that you can copy the url into mastodon search
-// and have it fetch the object.
+/// Generates the ActivityPub ID for a given object type and name.
+///
+/// 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 {
let point = match endpoint_type {
EndpointType::Community => "c",
}
}
-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 pkey = PKey::from_rsa(rsa).expect("sign::gen_keypair: parsing error");
- (
- pkey
- .public_key_to_pem()
- .expect("sign::gen_keypair: public key encoding error"),
- pkey
- .private_key_to_pem_pkcs8()
- .expect("sign::gen_keypair: private key encoding error"),
- )
-}
-
-pub fn gen_keypair_str() -> (String, String) {
- let (public_key, private_key) = gen_keypair();
+ let public_key = pkey
+ .public_key_to_pem()
+ .expect("sign::gen_keypair: public key encoding error");
+ let private_key = pkey
+ .private_key_to_pem_pkcs8()
+ .expect("sign::gen_keypair: private key encoding error");
(vec_bytes_to_str(public_key), vec_bytes_to_str(private_key))
}
post_id: String,
}
+/// Return the post json over HTTP.
pub async fn get_apub_post(
info: Path<PostQuery>,
db: web::Data<Pool<ConnectionManager<PgConnection>>>,
}
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> {
let mut page = Page::default();
let oprops: &mut ObjectProperties = page.as_mut();
}
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> {
let oprops = &page.object_props;
let creator_id = Url::parse(&oprops.get_attributed_to_xsd_any_uri().unwrap().to_string())?;
// For this example, we'll use the Extensible trait, the Extension trait, the Actor trait, and
// the Person type
use activitystreams::{actor::Actor, ext::Extension};
+use serde::{Deserialize, Serialize};
// The following is taken from here:
// 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")]
pub struct PublicKey {
pub id: String,
pub public_key_pem: String,
}
-#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)]
+#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PublicKeyExtension {
pub public_key: PublicKey,
user_name: String,
}
+// Turn a Lemmy user into an ActivityPub person and return it as json.
pub async fn get_apub_user(
info: Path<UserQuery>,
db: web::Data<Pool<ConnectionManager<PgConnection>>>,
}
impl UserForm {
+ /// Parse an ActivityPub person received from another instance into a Lemmy user.
pub fn from_person(person: &PersonExt) -> Result<Self, Error> {
let oprops = &person.base.base.object_props;
let aprops = &person.base.extension;
user_name: String,
}
+/// Handler for all incoming activities to user inboxes.
pub async fn user_inbox(
input: web::Json<UserAcceptedObjects>,
params: web::Query<Params>,
}
}
+/// Handle create activities and insert them in the database.
fn handle_create(create: &Create, conn: &PgConnection) -> Result<HttpResponse, Error> {
let page = create
.create_props
Ok(HttpResponse::Ok().finish())
}
+/// Handle update activities and insert them in the database.
fn handle_update(update: &Update, conn: &PgConnection) -> Result<HttpResponse, Error> {
let page = update
.update_props
Ok(HttpResponse::Ok().finish())
}
+/// Handle accepted follows.
fn handle_accept(_accept: &Accept, _conn: &PgConnection) -> Result<HttpResponse, Error> {
// TODO: make sure that we actually requested a follow
// TODO: at this point, indicate to the user that they are following the community