1 use crate::fetcher::post_or_comment::PostOrComment;
2 use activitypub_federation::config::{Data, UrlVerifier};
3 use async_trait::async_trait;
4 use lemmy_api_common::context::LemmyContext;
6 source::{activity::ReceivedActivity, instance::Instance, local_site::LocalSite},
7 utils::{ActualDbPool, DbPool},
9 use lemmy_utils::error::{LemmyError, LemmyErrorType, LemmyResult};
10 use moka::future::Cache;
11 use once_cell::sync::Lazy;
12 use std::{sync::Arc, time::Duration};
16 pub(crate) mod activity_lists;
18 pub(crate) mod collections;
21 pub(crate) mod mentions;
25 pub const FEDERATION_HTTP_FETCH_LIMIT: u32 = 50;
26 /// All incoming and outgoing federation actions read the blocklist/allowlist and slur filters
27 /// multiple times. This causes a huge number of database reads if we hit the db directly. So we
28 /// cache these values for a short time, which will already make a huge difference and ensures that
29 /// changes take effect quickly.
30 const BLOCKLIST_CACHE_DURATION: Duration = Duration::from_secs(60);
32 static CONTEXT: Lazy<Vec<serde_json::Value>> = Lazy::new(|| {
33 serde_json::from_str(include_str!("../assets/lemmy/context.json")).expect("parse context")
37 pub struct VerifyUrlData(pub ActualDbPool);
40 impl UrlVerifier for VerifyUrlData {
41 async fn verify(&self, url: &Url) -> Result<(), &'static str> {
42 let local_site_data = local_site_data_cached(&mut (&self.0).into())
44 .expect("read local site data");
45 check_apub_id_valid(url, &local_site_data).map_err(|err| match err {
47 error_type: LemmyErrorType::FederationDisabled,
49 } => "Federation disabled",
51 error_type: LemmyErrorType::DomainBlocked(_),
53 } => "Domain is blocked",
55 error_type: LemmyErrorType::DomainNotInAllowList(_),
57 } => "Domain is not in allowlist",
58 _ => "Failed validating apub id",
64 /// Checks if the ID is allowed for sending or receiving.
66 /// In particular, it checks for:
67 /// - federation being enabled (if its disabled, only local URLs are allowed)
68 /// - the correct scheme (either http or https)
69 /// - URL being in the allowlist (if it is active)
70 /// - URL not being in the blocklist (if it is active)
71 #[tracing::instrument(skip(local_site_data))]
72 fn check_apub_id_valid(apub_id: &Url, local_site_data: &LocalSiteData) -> Result<(), LemmyError> {
73 let domain = apub_id.domain().expect("apud id has domain").to_string();
78 .map(|l| l.federation_enabled)
81 Err(LemmyErrorType::FederationDisabled)?;
87 .any(|i| domain.eq(&i.domain))
89 Err(LemmyErrorType::DomainBlocked(domain.clone()))?;
92 // Only check this if there are instances in the allowlist
93 if !local_site_data.allowed_instances.is_empty()
97 .any(|i| domain.eq(&i.domain))
99 Err(LemmyErrorType::DomainNotInAllowList(domain))?;
106 pub(crate) struct LocalSiteData {
107 local_site: Option<LocalSite>,
108 allowed_instances: Vec<Instance>,
109 blocked_instances: Vec<Instance>,
112 pub(crate) async fn local_site_data_cached(
113 pool: &mut DbPool<'_>,
114 ) -> LemmyResult<Arc<LocalSiteData>> {
115 static CACHE: Lazy<Cache<(), Arc<LocalSiteData>>> = Lazy::new(|| {
118 .time_to_live(BLOCKLIST_CACHE_DURATION)
123 .try_get_with((), async {
124 let (local_site, allowed_instances, blocked_instances) =
125 lemmy_db_schema::try_join_with_pool!(pool => (
126 // LocalSite may be missing
128 Ok(LocalSite::read(pool).await.ok())
134 Ok::<_, diesel::result::Error>(Arc::new(LocalSiteData {
144 pub(crate) async fn check_apub_id_valid_with_strictness(
147 context: &LemmyContext,
148 ) -> Result<(), LemmyError> {
149 let domain = apub_id.domain().expect("apud id has domain").to_string();
150 let local_instance = context
152 .get_hostname_without_port()
153 .expect("local hostname is valid");
154 if domain == local_instance {
158 let local_site_data = local_site_data_cached(&mut context.pool()).await?;
159 check_apub_id_valid(apub_id, &local_site_data)?;
161 // Only check allowlist if this is a community, and there are instances in the allowlist
162 if is_strict && !local_site_data.allowed_instances.is_empty() {
163 // need to allow this explicitly because apub receive might contain objects from our local
165 let mut allowed_and_local = local_site_data
168 .map(|i| i.domain.clone())
169 .collect::<Vec<String>>();
170 let local_instance = context
172 .get_hostname_without_port()
173 .expect("local hostname is valid");
174 allowed_and_local.push(local_instance);
176 let domain = apub_id.domain().expect("apud id has domain").to_string();
177 if !allowed_and_local.contains(&domain) {
178 return Err(LemmyErrorType::FederationDisabledByStrictAllowList)?;
184 /// Store received activities in the database.
186 /// This ensures that the same activity doesnt get received and processed more than once, which
187 /// would be a waste of resources.
188 #[tracing::instrument(skip(data))]
189 async fn insert_received_activity(
191 data: &Data<LemmyContext>,
192 ) -> Result<(), LemmyError> {
193 ReceivedActivity::create(&mut data.pool(), &ap_id.clone().into()).await?;
197 #[async_trait::async_trait]
198 pub trait SendActivity: Sync {
199 type Response: Sync + Send + Clone;
201 async fn send_activity(
203 _response: &Self::Response,
204 _context: &Data<LemmyContext>,
205 ) -> Result<(), LemmyError> {