1 use crate::fetcher::post_or_comment::PostOrComment;
2 use activitypub_federation::config::{Data, UrlVerifier};
3 use async_trait::async_trait;
4 use futures::future::join3;
5 use lemmy_api_common::context::LemmyContext;
8 activity::{Activity, ActivityInsertForm},
10 local_site::LocalSite,
15 use lemmy_utils::error::{LemmyError, LemmyResult};
16 use moka::future::Cache;
17 use once_cell::sync::Lazy;
19 use std::{sync::Arc, time::Duration};
23 pub(crate) mod activity_lists;
25 pub(crate) mod collections;
28 pub(crate) mod mentions;
32 pub const FEDERATION_HTTP_FETCH_LIMIT: u32 = 50;
33 /// All incoming and outgoing federation actions read the blocklist/allowlist and slur filters
34 /// multiple times. This causes a huge number of database reads if we hit the db directly. So we
35 /// cache these values for a short time, which will already make a huge difference and ensures that
36 /// changes take effect quickly.
37 const BLOCKLIST_CACHE_DURATION: Duration = Duration::from_secs(60);
39 static CONTEXT: Lazy<Vec<serde_json::Value>> = Lazy::new(|| {
40 serde_json::from_str(include_str!("../assets/lemmy/context.json")).expect("parse context")
44 pub struct VerifyUrlData(pub DbPool);
47 impl UrlVerifier for VerifyUrlData {
48 async fn verify(&self, url: &Url) -> Result<(), &'static str> {
49 let local_site_data = local_site_data_cached(&self.0)
51 .expect("read local site data");
52 check_apub_id_valid(url, &local_site_data)?;
57 /// Checks if the ID is allowed for sending or receiving.
59 /// In particular, it checks for:
60 /// - federation being enabled (if its disabled, only local URLs are allowed)
61 /// - the correct scheme (either http or https)
62 /// - URL being in the allowlist (if it is active)
63 /// - URL not being in the blocklist (if it is active)
64 #[tracing::instrument(skip(local_site_data))]
65 fn check_apub_id_valid(apub_id: &Url, local_site_data: &LocalSiteData) -> Result<(), &'static str> {
66 let domain = apub_id.domain().expect("apud id has domain").to_string();
71 .map(|l| l.federation_enabled)
74 return Err("Federation disabled");
80 .any(|i| domain.eq(&i.domain))
82 return Err("Domain is blocked");
85 // Only check this if there are instances in the allowlist
86 if !local_site_data.allowed_instances.is_empty()
90 .any(|i| domain.eq(&i.domain))
92 return Err("Domain is not in allowlist");
99 pub(crate) struct LocalSiteData {
100 local_site: Option<LocalSite>,
101 allowed_instances: Vec<Instance>,
102 blocked_instances: Vec<Instance>,
105 pub(crate) async fn local_site_data_cached(pool: &DbPool) -> LemmyResult<Arc<LocalSiteData>> {
106 static CACHE: Lazy<Cache<(), Arc<LocalSiteData>>> = Lazy::new(|| {
109 .time_to_live(BLOCKLIST_CACHE_DURATION)
114 .try_get_with((), async {
115 let (local_site, allowed_instances, blocked_instances) = join3(
116 LocalSite::read(pool),
117 Instance::allowlist(pool),
118 Instance::blocklist(pool),
122 Ok::<_, diesel::result::Error>(Arc::new(LocalSiteData {
123 // LocalSite may be missing
124 local_site: local_site.ok(),
125 allowed_instances: allowed_instances?,
126 blocked_instances: blocked_instances?,
133 pub(crate) async fn check_apub_id_valid_with_strictness(
136 context: &LemmyContext,
137 ) -> Result<(), LemmyError> {
138 let domain = apub_id.domain().expect("apud id has domain").to_string();
139 let local_instance = context
141 .get_hostname_without_port()
142 .expect("local hostname is valid");
143 if domain == local_instance {
147 let local_site_data = local_site_data_cached(context.pool()).await?;
148 check_apub_id_valid(apub_id, &local_site_data).map_err(LemmyError::from_message)?;
150 // Only check allowlist if this is a community, and there are instances in the allowlist
151 if is_strict && !local_site_data.allowed_instances.is_empty() {
152 // need to allow this explicitly because apub receive might contain objects from our local
154 let mut allowed_and_local = local_site_data
157 .map(|i| i.domain.clone())
158 .collect::<Vec<String>>();
159 let local_instance = context
161 .get_hostname_without_port()
162 .expect("local hostname is valid");
163 allowed_and_local.push(local_instance);
165 let domain = apub_id.domain().expect("apud id has domain").to_string();
166 if !allowed_and_local.contains(&domain) {
167 return Err(LemmyError::from_message(
168 "Federation forbidden by strict allowlist",
175 /// Store a sent or received activity in the database.
177 /// Stored activities are served over the HTTP endpoint `GET /activities/{type_}/{id}`. This also
178 /// ensures that the same activity cannot be received more than once.
179 #[tracing::instrument(skip(data, activity))]
180 async fn insert_activity<T>(
185 data: &Data<LemmyContext>,
186 ) -> Result<(), LemmyError>
190 let ap_id = ap_id.clone().into();
191 let form = ActivityInsertForm {
193 data: serde_json::to_value(activity)?,
195 sensitive: Some(sensitive),
198 Activity::create(data.pool(), &form).await?;
202 #[async_trait::async_trait]
203 pub trait SendActivity: Sync {
204 type Response: Sync + Send;
206 async fn send_activity(
208 _response: &Self::Response,
209 _context: &Data<LemmyContext>,
210 ) -> Result<(), LemmyError> {