X-Git-Url: http://these/git/?a=blobdiff_plain;f=server%2Fsrc%2Fapub%2Fmod.rs;fp=server%2Fsrc%2Fapub%2Fmod.rs;h=7d2aee65c9101afc113ed27487489ac5218a2910;hb=dc94e58cbf7e7de10d97331a3056380a3416e0b0;hp=e224e2591636c033da7a4f65d0c02554b8cf2543;hpb=790b944031f9433be765936763d848ffa6e1b496;p=lemmy.git diff --git a/server/src/apub/mod.rs b/server/src/apub/mod.rs index e224e259..7d2aee65 100644 --- a/server/src/apub/mod.rs +++ b/server/src/apub/mod.rs @@ -1,107 +1,266 @@ +pub mod activities; +pub mod comment; pub mod community; +pub mod community_inbox; +pub mod extensions; +pub mod fetcher; pub mod post; +pub mod private_message; +pub mod shared_inbox; pub mod user; -use crate::Settings; - -use std::fmt::Display; - -#[cfg(test)] -mod tests { - use crate::db::community::Community; - use crate::db::post::Post; - use crate::db::user::User_; - use crate::db::{ListingType, SortType}; - use crate::{naive_now, Settings}; - - #[test] - fn test_person() { - let user = User_ { - id: 52, - name: "thom".into(), - fedi_name: "rrf".into(), - preferred_username: None, - password_encrypted: "here".into(), - email: None, - matrix_user_id: None, - avatar: None, - published: naive_now(), - admin: false, - banned: false, - updated: None, - show_nsfw: false, - theme: "darkly".into(), - default_sort_type: SortType::Hot as i16, - default_listing_type: ListingType::Subscribed as i16, - lang: "browser".into(), - show_avatars: true, - send_notifications_to_email: false, - }; - - let person = user.as_person(); - assert_eq!( - format!("https://{}/federation/u/thom", Settings::get().hostname), - person.object_props.id_string().unwrap() - ); +pub mod user_inbox; + +use crate::{ + apub::extensions::{ + group_extensions::GroupExtension, + page_extension::PageExtension, + signatures::{PublicKey, PublicKeyExtension}, + }, + convert_datetime, + db::user::User_, + routes::webfinger::WebFingerResponse, + MentionData, + Settings, +}; +use activitystreams::{ + actor::{properties::ApActorProperties, Group, Person}, + object::Page, +}; +use activitystreams_ext::{Ext1, Ext2, Ext3}; +use activitystreams_new::{activity::Follow, object::Tombstone, prelude::*}; +use actix_web::{body::Body, HttpResponse, Result}; +use chrono::NaiveDateTime; +use diesel::PgConnection; +use failure::Error; +use log::debug; +use serde::Serialize; +use url::Url; + +type GroupExt = Ext3; +type PersonExt = Ext2; +type PageExt = Ext1; + +pub static APUB_JSON_CONTENT_TYPE: &str = "application/activity+json"; + +pub enum EndpointType { + Community, + User, + Post, + Comment, + PrivateMessage, +} + +/// 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) +} + +/// Generates the ActivityPub ID for a given object type and ID. +pub fn make_apub_endpoint(endpoint_type: EndpointType, name: &str) -> Url { + let point = match endpoint_type { + EndpointType::Community => "c", + EndpointType::User => "u", + EndpointType::Post => "post", + EndpointType::Comment => "comment", + EndpointType::PrivateMessage => "private_message", + }; + + Url::parse(&format!( + "{}://{}/{}/{}", + get_apub_protocol_string(), + Settings::get().hostname, + point, + name + )) + .unwrap() +} + +pub fn get_apub_protocol_string() -> &'static str { + if Settings::get().federation.tls_enabled { + "https" + } else { + "http" + } +} + +// Checks if the ID has a valid format, correct scheme, and is in the allowed instance list. +fn is_apub_id_valid(apub_id: &Url) -> bool { + if apub_id.scheme() != get_apub_protocol_string() { + return false; } - #[test] - fn test_community() { - let community = Community { - id: 42, - name: "Test".into(), - title: "Test Title".into(), - description: Some("Test community".into()), - category_id: 32, - creator_id: 52, - removed: false, - published: naive_now(), - updated: Some(naive_now()), - deleted: false, - nsfw: false, - }; - - let group = community.as_group(); - assert_eq!( - format!("https://{}/federation/c/Test", Settings::get().hostname), - group.object_props.id_string().unwrap() - ); + let allowed_instances: Vec = Settings::get() + .federation + .allowed_instances + .split(',') + .map(|d| d.to_string()) + .collect(); + match apub_id.domain() { + Some(d) => allowed_instances.contains(&d.to_owned()), + None => false, } +} + +pub trait ToApub { + type Response; + fn to_apub(&self, conn: &PgConnection) -> Result; + fn to_tombstone(&self) -> Result; +} - #[test] - fn test_post() { - let post = Post { - id: 62, - name: "A test post".into(), - url: None, - body: None, - creator_id: 52, - community_id: 42, - published: naive_now(), - removed: false, - locked: false, - stickied: false, - nsfw: false, - deleted: false, - updated: None, - embed_title: None, - embed_description: None, - embed_html: None, - thumbnail_url: None, - }; - - let page = post.as_page(); - assert_eq!( - format!("https://{}/federation/post/62", Settings::get().hostname), - page.object_props.id_string().unwrap() - ); +/// Updated is actually the deletion time +fn create_tombstone( + deleted: bool, + object_id: &str, + updated: Option, + former_type: String, +) -> Result { + if deleted { + if let Some(updated) = updated { + let mut tombstone = Tombstone::new(); + tombstone.set_id(object_id.parse()?); + tombstone.set_former_type(former_type); + tombstone.set_deleted(convert_datetime(updated).into()); + Ok(tombstone) + } else { + Err(format_err!( + "Cant convert to tombstone because updated time was None." + )) + } + } else { + Err(format_err!( + "Cant convert object to tombstone if it wasnt deleted" + )) } } -pub fn make_apub_endpoint(point: S, value: T) -> String { +pub trait FromApub { + type ApubType; + fn from_apub(apub: &Self::ApubType, conn: &PgConnection) -> Result + where + Self: Sized; +} + +pub trait ApubObjectType { + fn send_create(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>; + fn send_update(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>; + fn send_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>; + fn send_undo_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>; + fn send_remove(&self, mod_: &User_, conn: &PgConnection) -> Result<(), Error>; + fn send_undo_remove(&self, mod_: &User_, conn: &PgConnection) -> Result<(), Error>; +} + +pub trait ApubLikeableType { + fn send_like(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>; + fn send_dislike(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>; + fn send_undo_like(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>; +} + +pub fn get_shared_inbox(actor_id: &str) -> String { + let url = Url::parse(actor_id).unwrap(); format!( - "https://{}/federation/{}/{}", - Settings::get().hostname, - point, - value + "{}://{}{}/inbox", + &url.scheme(), + &url.host_str().unwrap(), + if let Some(port) = url.port() { + format!(":{}", port) + } else { + "".to_string() + }, ) } + +pub trait ActorType { + fn actor_id(&self) -> String; + + fn public_key(&self) -> String; + fn private_key(&self) -> String; + + // These two have default impls, since currently a community can't follow anything, + // and a user can't be followed (yet) + #[allow(unused_variables)] + fn send_follow(&self, follow_actor_id: &str, conn: &PgConnection) -> Result<(), Error>; + fn send_unfollow(&self, follow_actor_id: &str, conn: &PgConnection) -> Result<(), Error>; + + #[allow(unused_variables)] + fn send_accept_follow(&self, follow: &Follow, conn: &PgConnection) -> Result<(), Error>; + + fn send_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>; + fn send_undo_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>; + + fn send_remove(&self, mod_: &User_, conn: &PgConnection) -> Result<(), Error>; + fn send_undo_remove(&self, mod_: &User_, conn: &PgConnection) -> Result<(), Error>; + + /// For a given community, returns the inboxes of all followers. + fn get_follower_inboxes(&self, conn: &PgConnection) -> Result, Error>; + + // TODO move these to the db rows + fn get_inbox_url(&self) -> String { + format!("{}/inbox", &self.actor_id()) + } + + fn get_shared_inbox_url(&self) -> String { + get_shared_inbox(&self.actor_id()) + } + + fn get_outbox_url(&self) -> String { + format!("{}/outbox", &self.actor_id()) + } + + fn get_followers_url(&self) -> String { + format!("{}/followers", &self.actor_id()) + } + + fn get_following_url(&self) -> String { + format!("{}/following", &self.actor_id()) + } + + fn get_liked_url(&self) -> String { + format!("{}/liked", &self.actor_id()) + } + + fn get_public_key_ext(&self) -> PublicKeyExtension { + PublicKey { + id: format!("{}#main-key", self.actor_id()), + owner: self.actor_id(), + public_key_pem: self.public_key(), + } + .to_ext() + } +} + +pub fn fetch_webfinger_url(mention: &MentionData) -> 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 text: String = attohttpc::get(&fetch_url).send()?.text()?; + let res: WebFingerResponse = serde_json::from_str(&text)?; + let link = res + .links + .iter() + .find(|l| l.type_.eq(&Some("application/activity+json".to_string()))) + .ok_or_else(|| format_err!("No application/activity+json link found."))?; + link + .href + .to_owned() + .ok_or_else(|| format_err!("No href found.")) +}