2 pub mod activity_queue;
9 pub mod private_message;
14 group_extensions::GroupExtension,
15 page_extension::PageExtension,
16 signatures::{PublicKey, PublicKeyExtension},
18 request::{retry, RecvError},
19 routes::webfinger::WebFingerResponse,
23 use activitystreams::{
25 actor::{ApActor, Group, Person},
28 object::{Page, Tombstone},
31 use activitystreams_ext::{Ext1, Ext2};
32 use actix_web::{body::Body, HttpResponse};
33 use anyhow::{anyhow, Context};
34 use chrono::NaiveDateTime;
35 use lemmy_db::{activity::do_insert_activity, user::User_};
36 use lemmy_structs::blocking;
38 apub::get_apub_protocol_string,
41 utils::{convert_datetime, MentionData},
47 use url::{ParseError, Url};
49 type GroupExt = Ext2<ApActor<Group>, GroupExtension, PublicKeyExtension>;
50 type PersonExt = Ext1<ApActor<Person>, PublicKeyExtension>;
51 type PageExt = Ext1<Page, PageExtension>;
53 pub static APUB_JSON_CONTENT_TYPE: &str = "application/activity+json";
55 /// Convert the data to json and turn it into an HTTP Response with the correct ActivityPub
57 fn create_apub_response<T>(data: &T) -> HttpResponse<Body>
62 .content_type(APUB_JSON_CONTENT_TYPE)
66 fn create_apub_tombstone_response<T>(data: &T) -> HttpResponse<Body>
71 .content_type(APUB_JSON_CONTENT_TYPE)
75 // Checks if the ID has a valid format, correct scheme, and is in the allowed instance list.
76 fn check_is_apub_id_valid(apub_id: &Url) -> Result<(), LemmyError> {
77 let settings = Settings::get();
78 let domain = apub_id.domain().context(location_info!())?.to_string();
79 let local_instance = settings
82 .collect::<Vec<&str>>()
84 .context(location_info!())?
87 if !settings.federation.enabled {
88 return if domain == local_instance {
93 "Trying to connect with {}, but federation is disabled",
101 if apub_id.scheme() != get_apub_protocol_string() {
102 return Err(anyhow!("invalid apub id scheme: {:?}", apub_id.scheme()).into());
105 let mut allowed_instances = Settings::get().get_allowed_instances();
106 let blocked_instances = Settings::get().get_blocked_instances();
108 if !allowed_instances.is_empty() {
109 // need to allow this explicitly because apub activities might contain objects from our local
110 // instance. split is needed to remove the port in our federation test setup.
111 allowed_instances.push(local_instance);
113 if allowed_instances.contains(&domain) {
116 Err(anyhow!("{} not in federation allowlist", domain).into())
118 } else if !blocked_instances.is_empty() {
119 if blocked_instances.contains(&domain) {
120 Err(anyhow!("{} is in federation blocklist", domain).into())
125 panic!("Invalid config, both allowed_instances and blocked_instances are specified");
129 #[async_trait::async_trait(?Send)]
132 async fn to_apub(&self, pool: &DbPool) -> Result<Self::Response, LemmyError>;
133 fn to_tombstone(&self) -> Result<Tombstone, LemmyError>;
136 /// Updated is actually the deletion time
137 fn create_tombstone<T>(
140 updated: Option<NaiveDateTime>,
142 ) -> Result<Tombstone, LemmyError>
147 if let Some(updated) = updated {
148 let mut tombstone = Tombstone::new();
149 tombstone.set_id(object_id.parse()?);
150 tombstone.set_former_type(former_type.to_string());
151 tombstone.set_deleted(convert_datetime(updated));
154 Err(anyhow!("Cant convert to tombstone because updated time was None.").into())
157 Err(anyhow!("Cant convert object to tombstone if it wasnt deleted").into())
161 #[async_trait::async_trait(?Send)]
164 /// Converts an object from ActivityPub type to Lemmy internal type.
166 /// * `apub` The object to read from
167 /// * `context` LemmyContext which holds DB pool, HTTP client etc
168 /// * `expected_domain` If present, ensure that the apub object comes from the same domain as
172 apub: &Self::ApubType,
173 context: &LemmyContext,
174 expected_domain: Option<Url>,
175 ) -> Result<Self, LemmyError>
180 #[async_trait::async_trait(?Send)]
181 pub trait ApubObjectType {
182 async fn send_create(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError>;
183 async fn send_update(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError>;
184 async fn send_delete(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError>;
185 async fn send_undo_delete(
188 context: &LemmyContext,
189 ) -> Result<(), LemmyError>;
190 async fn send_remove(&self, mod_: &User_, context: &LemmyContext) -> Result<(), LemmyError>;
191 async fn send_undo_remove(&self, mod_: &User_, context: &LemmyContext) -> Result<(), LemmyError>;
194 pub(in crate::apub) fn check_actor_domain<T, Kind>(
196 expected_domain: Option<Url>,
197 ) -> Result<String, LemmyError>
199 T: Base + AsBase<Kind>,
201 let actor_id = if let Some(url) = expected_domain {
202 let domain = url.domain().context(location_info!())?;
203 apub.id(domain)?.context(location_info!())?
205 let actor_id = apub.id_unchecked().context(location_info!())?;
206 check_is_apub_id_valid(&actor_id)?;
209 Ok(actor_id.to_string())
212 #[async_trait::async_trait(?Send)]
213 pub trait ApubLikeableType {
214 async fn send_like(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError>;
215 async fn send_dislike(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError>;
216 async fn send_undo_like(&self, creator: &User_, context: &LemmyContext)
217 -> Result<(), LemmyError>;
220 #[async_trait::async_trait(?Send)]
221 pub trait ActorType {
222 fn actor_id_str(&self) -> String;
224 // TODO: every actor should have a public key, so this shouldnt be an option (needs to be fixed in db)
225 fn public_key(&self) -> Option<String>;
226 fn private_key(&self) -> Option<String>;
228 /// numeric id in the database, used for insert_activity
229 fn user_id(&self) -> i32;
231 // These two have default impls, since currently a community can't follow anything,
232 // and a user can't be followed (yet)
233 #[allow(unused_variables)]
234 async fn send_follow(
236 follow_actor_id: &Url,
237 context: &LemmyContext,
238 ) -> Result<(), LemmyError>;
239 async fn send_unfollow(
241 follow_actor_id: &Url,
242 context: &LemmyContext,
243 ) -> Result<(), LemmyError>;
245 #[allow(unused_variables)]
246 async fn send_accept_follow(
249 context: &LemmyContext,
250 ) -> Result<(), LemmyError>;
252 async fn send_delete(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError>;
253 async fn send_undo_delete(
256 context: &LemmyContext,
257 ) -> Result<(), LemmyError>;
259 async fn send_remove(&self, mod_: &User_, context: &LemmyContext) -> Result<(), LemmyError>;
260 async fn send_undo_remove(&self, mod_: &User_, context: &LemmyContext) -> Result<(), LemmyError>;
262 /// For a given community, returns the inboxes of all followers.
263 async fn get_follower_inboxes(&self, pool: &DbPool) -> Result<Vec<Url>, LemmyError>;
265 fn actor_id(&self) -> Result<Url, ParseError> {
266 Url::parse(&self.actor_id_str())
269 // TODO move these to the db rows
270 fn get_inbox_url(&self) -> Result<Url, ParseError> {
271 Url::parse(&format!("{}/inbox", &self.actor_id_str()))
274 fn get_shared_inbox_url(&self) -> Result<Url, LemmyError> {
275 let actor_id = self.actor_id()?;
279 &actor_id.host_str().context(location_info!())?,
280 if let Some(port) = actor_id.port() {
286 Ok(Url::parse(&url)?)
289 fn get_outbox_url(&self) -> Result<Url, ParseError> {
290 Url::parse(&format!("{}/outbox", &self.actor_id_str()))
293 fn get_followers_url(&self) -> Result<Url, ParseError> {
294 Url::parse(&format!("{}/followers", &self.actor_id_str()))
297 fn get_following_url(&self) -> String {
298 format!("{}/following", &self.actor_id_str())
301 fn get_liked_url(&self) -> String {
302 format!("{}/liked", &self.actor_id_str())
305 fn get_public_key_ext(&self) -> Result<PublicKeyExtension, LemmyError> {
308 id: format!("{}#main-key", self.actor_id_str()),
309 owner: self.actor_id_str(),
310 public_key_pem: self.public_key().context(location_info!())?,
317 pub async fn fetch_webfinger_url(
318 mention: &MentionData,
320 ) -> Result<Url, LemmyError> {
321 let fetch_url = format!(
322 "{}://{}/.well-known/webfinger?resource=acct:{}@{}",
323 get_apub_protocol_string(),
328 debug!("Fetching webfinger url: {}", &fetch_url);
330 let response = retry(|| client.get(&fetch_url).send()).await?;
332 let res: WebFingerResponse = response
335 .map_err(|e| RecvError(e.to_string()))?;
340 .find(|l| l.type_.eq(&Some("application/activity+json".to_string())))
341 .ok_or_else(|| anyhow!("No application/activity+json link found."))?;
345 .map(|u| Url::parse(&u))
347 .ok_or_else(|| anyhow!("No href found.").into())
350 pub async fn insert_activity<T>(
355 ) -> Result<(), LemmyError>
357 T: Serialize + std::fmt::Debug + Send + 'static,
359 blocking(pool, move |conn| {
360 do_insert_activity(conn, user_id, &data, local)