]> Untitled Git - lemmy.git/blob - server/src/apub/mod.rs
7d2aee65c9101afc113ed27487489ac5218a2910
[lemmy.git] / server / src / apub / mod.rs
1 pub mod activities;
2 pub mod comment;
3 pub mod community;
4 pub mod community_inbox;
5 pub mod extensions;
6 pub mod fetcher;
7 pub mod post;
8 pub mod private_message;
9 pub mod shared_inbox;
10 pub mod user;
11 pub mod user_inbox;
12
13 use crate::{
14   apub::extensions::{
15     group_extensions::GroupExtension,
16     page_extension::PageExtension,
17     signatures::{PublicKey, PublicKeyExtension},
18   },
19   convert_datetime,
20   db::user::User_,
21   routes::webfinger::WebFingerResponse,
22   MentionData,
23   Settings,
24 };
25 use activitystreams::{
26   actor::{properties::ApActorProperties, Group, Person},
27   object::Page,
28 };
29 use activitystreams_ext::{Ext1, Ext2, Ext3};
30 use activitystreams_new::{activity::Follow, object::Tombstone, prelude::*};
31 use actix_web::{body::Body, HttpResponse, Result};
32 use chrono::NaiveDateTime;
33 use diesel::PgConnection;
34 use failure::Error;
35 use log::debug;
36 use serde::Serialize;
37 use url::Url;
38
39 type GroupExt = Ext3<Group, GroupExtension, ApActorProperties, PublicKeyExtension>;
40 type PersonExt = Ext2<Person, ApActorProperties, PublicKeyExtension>;
41 type PageExt = Ext1<Page, PageExtension>;
42
43 pub static APUB_JSON_CONTENT_TYPE: &str = "application/activity+json";
44
45 pub enum EndpointType {
46   Community,
47   User,
48   Post,
49   Comment,
50   PrivateMessage,
51 }
52
53 /// Convert the data to json and turn it into an HTTP Response with the correct ActivityPub
54 /// headers.
55 fn create_apub_response<T>(data: &T) -> HttpResponse<Body>
56 where
57   T: Serialize,
58 {
59   HttpResponse::Ok()
60     .content_type(APUB_JSON_CONTENT_TYPE)
61     .json(data)
62 }
63
64 fn create_apub_tombstone_response<T>(data: &T) -> HttpResponse<Body>
65 where
66   T: Serialize,
67 {
68   HttpResponse::Gone()
69     .content_type(APUB_JSON_CONTENT_TYPE)
70     .json(data)
71 }
72
73 /// Generates the ActivityPub ID for a given object type and ID.
74 pub fn make_apub_endpoint(endpoint_type: EndpointType, name: &str) -> Url {
75   let point = match endpoint_type {
76     EndpointType::Community => "c",
77     EndpointType::User => "u",
78     EndpointType::Post => "post",
79     EndpointType::Comment => "comment",
80     EndpointType::PrivateMessage => "private_message",
81   };
82
83   Url::parse(&format!(
84     "{}://{}/{}/{}",
85     get_apub_protocol_string(),
86     Settings::get().hostname,
87     point,
88     name
89   ))
90   .unwrap()
91 }
92
93 pub fn get_apub_protocol_string() -> &'static str {
94   if Settings::get().federation.tls_enabled {
95     "https"
96   } else {
97     "http"
98   }
99 }
100
101 // Checks if the ID has a valid format, correct scheme, and is in the allowed instance list.
102 fn is_apub_id_valid(apub_id: &Url) -> bool {
103   if apub_id.scheme() != get_apub_protocol_string() {
104     return false;
105   }
106
107   let allowed_instances: Vec<String> = Settings::get()
108     .federation
109     .allowed_instances
110     .split(',')
111     .map(|d| d.to_string())
112     .collect();
113   match apub_id.domain() {
114     Some(d) => allowed_instances.contains(&d.to_owned()),
115     None => false,
116   }
117 }
118
119 pub trait ToApub {
120   type Response;
121   fn to_apub(&self, conn: &PgConnection) -> Result<Self::Response, Error>;
122   fn to_tombstone(&self) -> Result<Tombstone, Error>;
123 }
124
125 /// Updated is actually the deletion time
126 fn create_tombstone(
127   deleted: bool,
128   object_id: &str,
129   updated: Option<NaiveDateTime>,
130   former_type: String,
131 ) -> Result<Tombstone, Error> {
132   if deleted {
133     if let Some(updated) = updated {
134       let mut tombstone = Tombstone::new();
135       tombstone.set_id(object_id.parse()?);
136       tombstone.set_former_type(former_type);
137       tombstone.set_deleted(convert_datetime(updated).into());
138       Ok(tombstone)
139     } else {
140       Err(format_err!(
141         "Cant convert to tombstone because updated time was None."
142       ))
143     }
144   } else {
145     Err(format_err!(
146       "Cant convert object to tombstone if it wasnt deleted"
147     ))
148   }
149 }
150
151 pub trait FromApub {
152   type ApubType;
153   fn from_apub(apub: &Self::ApubType, conn: &PgConnection) -> Result<Self, Error>
154   where
155     Self: Sized;
156 }
157
158 pub trait ApubObjectType {
159   fn send_create(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>;
160   fn send_update(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>;
161   fn send_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>;
162   fn send_undo_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>;
163   fn send_remove(&self, mod_: &User_, conn: &PgConnection) -> Result<(), Error>;
164   fn send_undo_remove(&self, mod_: &User_, conn: &PgConnection) -> Result<(), Error>;
165 }
166
167 pub trait ApubLikeableType {
168   fn send_like(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>;
169   fn send_dislike(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>;
170   fn send_undo_like(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>;
171 }
172
173 pub fn get_shared_inbox(actor_id: &str) -> String {
174   let url = Url::parse(actor_id).unwrap();
175   format!(
176     "{}://{}{}/inbox",
177     &url.scheme(),
178     &url.host_str().unwrap(),
179     if let Some(port) = url.port() {
180       format!(":{}", port)
181     } else {
182       "".to_string()
183     },
184   )
185 }
186
187 pub trait ActorType {
188   fn actor_id(&self) -> String;
189
190   fn public_key(&self) -> String;
191   fn private_key(&self) -> String;
192
193   // These two have default impls, since currently a community can't follow anything,
194   // and a user can't be followed (yet)
195   #[allow(unused_variables)]
196   fn send_follow(&self, follow_actor_id: &str, conn: &PgConnection) -> Result<(), Error>;
197   fn send_unfollow(&self, follow_actor_id: &str, conn: &PgConnection) -> Result<(), Error>;
198
199   #[allow(unused_variables)]
200   fn send_accept_follow(&self, follow: &Follow, conn: &PgConnection) -> Result<(), Error>;
201
202   fn send_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>;
203   fn send_undo_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>;
204
205   fn send_remove(&self, mod_: &User_, conn: &PgConnection) -> Result<(), Error>;
206   fn send_undo_remove(&self, mod_: &User_, conn: &PgConnection) -> Result<(), Error>;
207
208   /// For a given community, returns the inboxes of all followers.
209   fn get_follower_inboxes(&self, conn: &PgConnection) -> Result<Vec<String>, Error>;
210
211   // TODO move these to the db rows
212   fn get_inbox_url(&self) -> String {
213     format!("{}/inbox", &self.actor_id())
214   }
215
216   fn get_shared_inbox_url(&self) -> String {
217     get_shared_inbox(&self.actor_id())
218   }
219
220   fn get_outbox_url(&self) -> String {
221     format!("{}/outbox", &self.actor_id())
222   }
223
224   fn get_followers_url(&self) -> String {
225     format!("{}/followers", &self.actor_id())
226   }
227
228   fn get_following_url(&self) -> String {
229     format!("{}/following", &self.actor_id())
230   }
231
232   fn get_liked_url(&self) -> String {
233     format!("{}/liked", &self.actor_id())
234   }
235
236   fn get_public_key_ext(&self) -> PublicKeyExtension {
237     PublicKey {
238       id: format!("{}#main-key", self.actor_id()),
239       owner: self.actor_id(),
240       public_key_pem: self.public_key(),
241     }
242     .to_ext()
243   }
244 }
245
246 pub fn fetch_webfinger_url(mention: &MentionData) -> Result<String, Error> {
247   let fetch_url = format!(
248     "{}://{}/.well-known/webfinger?resource=acct:{}@{}",
249     get_apub_protocol_string(),
250     mention.domain,
251     mention.name,
252     mention.domain
253   );
254   debug!("Fetching webfinger url: {}", &fetch_url);
255   let text: String = attohttpc::get(&fetch_url).send()?.text()?;
256   let res: WebFingerResponse = serde_json::from_str(&text)?;
257   let link = res
258     .links
259     .iter()
260     .find(|l| l.type_.eq(&Some("application/activity+json".to_string())))
261     .ok_or_else(|| format_err!("No application/activity+json link found."))?;
262   link
263     .href
264     .to_owned()
265     .ok_or_else(|| format_err!("No href found."))
266 }