]> Untitled Git - lemmy.git/blobdiff - server/src/apub/mod.rs
Merge branch 'master' into federation_merge_from_master_2
[lemmy.git] / server / src / apub / mod.rs
index e224e2591636c033da7a4f65d0c02554b8cf2543..7d2aee65c9101afc113ed27487489ac5218a2910 100644 (file)
+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<Group, GroupExtension, ApActorProperties, PublicKeyExtension>;
+type PersonExt = Ext2<Person, ApActorProperties, PublicKeyExtension>;
+type PageExt = Ext1<Page, PageExtension>;
+
+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<T>(data: &T) -> HttpResponse<Body>
+where
+  T: Serialize,
+{
+  HttpResponse::Ok()
+    .content_type(APUB_JSON_CONTENT_TYPE)
+    .json(data)
+}
+
+fn create_apub_tombstone_response<T>(data: &T) -> HttpResponse<Body>
+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<String> = 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<Self::Response, Error>;
+  fn to_tombstone(&self) -> Result<Tombstone, Error>;
+}
 
-  #[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<NaiveDateTime>,
+  former_type: String,
+) -> Result<Tombstone, Error> {
+  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<S: Display, T: Display>(point: S, value: T) -> String {
+pub trait FromApub {
+  type ApubType;
+  fn from_apub(apub: &Self::ApubType, conn: &PgConnection) -> Result<Self, Error>
+  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<Vec<String>, 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<String, Error> {
+  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."))
+}