2 extern crate lazy_static;
5 pub mod activity_queue;
14 group_extension::GroupExtension,
15 person_extension::PersonExtension,
16 signatures::{PublicKey, PublicKeyExtension},
18 fetcher::community::get_or_fetch_and_upsert_community,
20 use activitystreams::{
24 object::{ApObject, AsObject, ObjectExt},
26 use activitystreams_ext::Ext2;
27 use anyhow::{anyhow, Context};
29 use lemmy_api_common::blocking;
30 use lemmy_db_queries::{source::activity::Activity_, ApubObject, DbPool};
31 use lemmy_db_schema::{
36 person::{Person as DbPerson, Person},
38 private_message::PrivateMessage,
43 use lemmy_db_views_actor::community_person_ban_view::CommunityPersonBanView;
44 use lemmy_utils::{location_info, settings::structs::Settings, LemmyError};
45 use lemmy_websocket::LemmyContext;
48 use url::{ParseError, Url};
50 /// Activitystreams type for community
52 Ext2<actor::ApActor<ApObject<actor::Group>>, GroupExtension, PublicKeyExtension>;
53 /// Activitystreams type for person
55 Ext2<actor::ApActor<ApObject<actor::Actor<UserTypes>>>, PersonExtension, PublicKeyExtension>;
56 pub type SiteExt = actor::ApActor<ApObject<actor::Service>>;
58 #[derive(Clone, Copy, Debug, serde::Deserialize, serde::Serialize, PartialEq)]
64 pub static APUB_JSON_CONTENT_TYPE: &str = "application/activity+json";
66 /// Checks if the ID is allowed for sending or receiving.
68 /// In particular, it checks for:
69 /// - federation being enabled (if its disabled, only local URLs are allowed)
70 /// - the correct scheme (either http or https)
71 /// - URL being in the allowlist (if it is active)
72 /// - URL not being in the blocklist (if it is active)
74 pub fn check_is_apub_id_valid(apub_id: &Url, use_strict_allowlist: bool) -> Result<(), LemmyError> {
75 let settings = Settings::get();
76 let domain = apub_id.domain().context(location_info!())?.to_string();
77 let local_instance = settings.get_hostname_without_port()?;
79 if !settings.federation.enabled {
80 return if domain == local_instance {
85 "Trying to connect with {}, but federation is disabled",
93 let host = apub_id.host_str().context(location_info!())?;
94 let host_as_ip = host.parse::<IpAddr>();
95 if host == "localhost" || host_as_ip.is_ok() {
96 return Err(anyhow!("invalid hostname {}: {}", host, apub_id).into());
99 if apub_id.scheme() != Settings::get().get_protocol_string() {
100 return Err(anyhow!("invalid apub id scheme {}: {}", apub_id.scheme(), apub_id).into());
103 // TODO: might be good to put the part above in one method, and below in another
104 // (which only gets called in apub::objects)
105 // -> no that doesnt make sense, we still need the code below for blocklist and strict allowlist
106 if let Some(blocked) = Settings::get().federation.blocked_instances {
107 if blocked.contains(&domain) {
108 return Err(anyhow!("{} is in federation blocklist", domain).into());
112 if let Some(mut allowed) = Settings::get().federation.allowed_instances {
113 // Only check allowlist if this is a community, or strict allowlist is enabled.
114 let strict_allowlist = Settings::get().federation.strict_allowlist;
115 if use_strict_allowlist || strict_allowlist {
116 // need to allow this explicitly because apub receive might contain objects from our local
118 allowed.push(local_instance);
120 if !allowed.contains(&domain) {
121 return Err(anyhow!("{} not in federation allowlist", domain).into());
129 /// Common functions for ActivityPub objects, which are implemented by most (but not all) objects
130 /// and actors in Lemmy.
131 #[async_trait::async_trait(?Send)]
132 pub trait ApubObjectType {
133 async fn send_delete(&self, creator: &DbPerson, context: &LemmyContext)
134 -> Result<(), LemmyError>;
135 async fn send_undo_delete(
138 context: &LemmyContext,
139 ) -> Result<(), LemmyError>;
140 async fn send_remove(&self, mod_: &DbPerson, context: &LemmyContext) -> Result<(), LemmyError>;
141 async fn send_undo_remove(
144 context: &LemmyContext,
145 ) -> Result<(), LemmyError>;
148 /// Common methods provided by ActivityPub actors (community and person). Not all methods are
149 /// implemented by all actors.
150 pub trait ActorType {
151 fn is_local(&self) -> bool;
152 fn actor_id(&self) -> Url;
153 fn name(&self) -> String;
155 // TODO: every actor should have a public key, so this shouldnt be an option (needs to be fixed in db)
156 fn public_key(&self) -> Option<String>;
157 fn private_key(&self) -> Option<String>;
159 fn get_shared_inbox_or_inbox_url(&self) -> Url;
161 /// Outbox URL is not generally used by Lemmy, so it can be generated on the fly (but only for
163 fn get_outbox_url(&self) -> Result<Url, LemmyError> {
165 if !self.is_local() {
166 return Err(anyhow!("get_outbox_url() called for remote actor").into());
169 Ok(Url::parse(&format!("{}/outbox", &self.actor_id()))?)
172 fn get_public_key_ext(&self) -> Result<PublicKeyExtension, LemmyError> {
175 id: format!("{}#main-key", self.actor_id()),
176 owner: self.actor_id(),
177 public_key_pem: self.public_key().context(location_info!())?,
184 #[async_trait::async_trait(?Send)]
185 pub trait CommunityType {
186 fn followers_url(&self) -> Url;
187 async fn get_follower_inboxes(&self, pool: &DbPool) -> Result<Vec<Url>, LemmyError>;
188 async fn send_accept_follow(
191 context: &LemmyContext,
192 ) -> Result<(), LemmyError>;
194 async fn send_update(&self, mod_: Person, context: &LemmyContext) -> Result<(), LemmyError>;
195 async fn send_delete(&self, mod_: Person, context: &LemmyContext) -> Result<(), LemmyError>;
196 async fn send_undo_delete(&self, mod_: Person, context: &LemmyContext) -> Result<(), LemmyError>;
198 async fn send_remove(&self, context: &LemmyContext) -> Result<(), LemmyError>;
199 async fn send_undo_remove(&self, context: &LemmyContext) -> Result<(), LemmyError>;
201 async fn send_announce(
205 context: &LemmyContext,
206 ) -> Result<(), LemmyError>;
208 async fn send_add_mod(
212 context: &LemmyContext,
213 ) -> Result<(), LemmyError>;
214 async fn send_remove_mod(
218 context: &LemmyContext,
219 ) -> Result<(), LemmyError>;
221 async fn send_block_user(
224 blocked_user: Person,
225 context: &LemmyContext,
226 ) -> Result<(), LemmyError>;
227 async fn send_undo_block_user(
230 blocked_user: Person,
231 context: &LemmyContext,
232 ) -> Result<(), LemmyError>;
235 #[async_trait::async_trait(?Send)]
237 async fn send_follow(
239 follow_actor_id: &Url,
240 context: &LemmyContext,
241 ) -> Result<(), LemmyError>;
242 async fn send_unfollow(
244 follow_actor_id: &Url,
245 context: &LemmyContext,
246 ) -> Result<(), LemmyError>;
249 pub enum EndpointType {
257 /// Generates an apub endpoint for a given domain, IE xyz.tld
258 pub fn generate_apub_endpoint_for_domain(
259 endpoint_type: EndpointType,
262 ) -> Result<DbUrl, ParseError> {
263 let point = match endpoint_type {
264 EndpointType::Community => "c",
265 EndpointType::Person => "u",
266 EndpointType::Post => "post",
267 EndpointType::Comment => "comment",
268 EndpointType::PrivateMessage => "private_message",
271 Ok(Url::parse(&format!("{}/{}/{}", domain, point, name))?.into())
274 /// Generates the ActivityPub ID for a given object type and ID.
275 pub fn generate_apub_endpoint(
276 endpoint_type: EndpointType,
278 ) -> Result<DbUrl, ParseError> {
279 generate_apub_endpoint_for_domain(
282 &Settings::get().get_protocol_and_hostname(),
286 pub fn generate_followers_url(actor_id: &DbUrl) -> Result<DbUrl, ParseError> {
287 Ok(Url::parse(&format!("{}/followers", actor_id))?.into())
290 pub fn generate_inbox_url(actor_id: &DbUrl) -> Result<DbUrl, ParseError> {
291 Ok(Url::parse(&format!("{}/inbox", actor_id))?.into())
294 pub fn generate_shared_inbox_url(actor_id: &DbUrl) -> Result<DbUrl, LemmyError> {
295 let actor_id: Url = actor_id.clone().into();
299 &actor_id.host_str().context(location_info!())?,
300 if let Some(port) = actor_id.port() {
306 Ok(Url::parse(&url)?.into())
309 pub fn generate_moderators_url(community_id: &DbUrl) -> Result<DbUrl, LemmyError> {
310 Ok(Url::parse(&format!("{}/moderators", community_id))?.into())
313 /// Takes in a shortname of the type dessalines@xyz.tld or dessalines (assumed to be local), and outputs the actor id.
314 /// Used in the API for communities and users.
315 pub fn build_actor_id_from_shortname(
316 endpoint_type: EndpointType,
318 ) -> Result<DbUrl, ParseError> {
319 let split = short_name.split('@').collect::<Vec<&str>>();
323 // If there's no @, its local
324 let domain = if split.len() == 1 {
325 Settings::get().get_protocol_and_hostname()
327 format!("{}://{}", Settings::get().get_protocol_string(), split[1])
330 generate_apub_endpoint_for_domain(endpoint_type, name, &domain)
333 /// Store a sent or received activity in the database, for logging purposes. These records are not
335 pub async fn insert_activity<T>(
341 ) -> Result<(), LemmyError>
343 T: Serialize + std::fmt::Debug + Send + 'static,
345 let ap_id = ap_id.to_owned().into();
346 blocking(pool, move |conn| {
347 Activity::insert(conn, ap_id, &activity, local, sensitive)
353 pub enum PostOrComment {
354 Comment(Box<Comment>),
359 pub(crate) fn ap_id(&self) -> Url {
361 PostOrComment::Post(p) => p.ap_id.clone(),
362 PostOrComment::Comment(c) => c.ap_id.clone(),
368 /// Tries to find a post or comment in the local database, without any network requests.
369 /// This is used to handle deletions and removals, because in case we dont have the object, we can
370 /// simply ignore the activity.
371 pub async fn find_post_or_comment_by_id(
372 context: &LemmyContext,
374 ) -> Result<PostOrComment, LemmyError> {
375 let ap_id = apub_id.clone();
376 let post = blocking(context.pool(), move |conn| {
377 Post::read_from_apub_id(conn, &ap_id.into())
380 if let Ok(p) = post {
381 return Ok(PostOrComment::Post(Box::new(p)));
384 let ap_id = apub_id.clone();
385 let comment = blocking(context.pool(), move |conn| {
386 Comment::read_from_apub_id(conn, &ap_id.into())
389 if let Ok(c) = comment {
390 return Ok(PostOrComment::Comment(Box::new(c)));
398 Comment(Box<Comment>),
400 Community(Box<Community>),
401 Person(Box<DbPerson>),
402 PrivateMessage(Box<PrivateMessage>),
405 pub async fn find_object_by_id(context: &LemmyContext, apub_id: Url) -> Result<Object, LemmyError> {
406 let ap_id = apub_id.clone();
407 if let Ok(pc) = find_post_or_comment_by_id(context, ap_id.to_owned()).await {
409 PostOrComment::Post(p) => Object::Post(Box::new(*p)),
410 PostOrComment::Comment(c) => Object::Comment(Box::new(*c)),
414 let ap_id = apub_id.clone();
415 let person = blocking(context.pool(), move |conn| {
416 DbPerson::read_from_apub_id(conn, &ap_id.into())
419 if let Ok(u) = person {
420 return Ok(Object::Person(Box::new(u)));
423 let ap_id = apub_id.clone();
424 let community = blocking(context.pool(), move |conn| {
425 Community::read_from_apub_id(conn, &ap_id.into())
428 if let Ok(c) = community {
429 return Ok(Object::Community(Box::new(c)));
432 let private_message = blocking(context.pool(), move |conn| {
433 PrivateMessage::read_from_apub_id(conn, &apub_id.into())
436 if let Ok(pm) = private_message {
437 return Ok(Object::PrivateMessage(Box::new(pm)));
443 pub async fn check_community_or_site_ban(
445 community_id: CommunityId,
447 ) -> Result<(), LemmyError> {
449 return Err(anyhow!("Person is banned from site").into());
451 let person_id = person.id;
453 move |conn: &'_ _| CommunityPersonBanView::get(conn, person_id, community_id).is_ok();
454 if blocking(pool, is_banned).await? {
455 return Err(anyhow!("Person is banned from community").into());
461 pub fn get_activity_to_and_cc<T, Kind>(activity: &T) -> Vec<Url>
465 let mut to_and_cc = vec![];
466 if let Some(to) = activity.to() {
467 let to = to.to_owned().unwrap_to_vec();
470 .map(|t| t.as_xsd_any_uri())
472 .map(|t| t.to_owned())
474 to_and_cc.append(&mut to);
476 if let Some(cc) = activity.cc() {
477 let cc = cc.to_owned().unwrap_to_vec();
480 .map(|c| c.as_xsd_any_uri())
482 .map(|c| c.to_owned())
484 to_and_cc.append(&mut cc);
489 pub async fn get_community_from_to_or_cc<T, Kind>(
491 context: &LemmyContext,
492 request_counter: &mut i32,
493 ) -> Result<Community, LemmyError>
497 for cid in get_activity_to_and_cc(activity) {
498 let community = get_or_fetch_and_upsert_community(&cid, context, request_counter).await;
499 if community.is_ok() {