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;
7 activity::{Activity, ActivityInsertForm},
14 use lemmy_utils::{error::LemmyError, settings::structs::Settings};
15 use once_cell::sync::Lazy;
20 pub(crate) mod activity_lists;
22 pub(crate) mod collections;
25 pub(crate) mod mentions;
29 pub const FEDERATION_HTTP_FETCH_LIMIT: u32 = 50;
31 static CONTEXT: Lazy<Vec<serde_json::Value>> = Lazy::new(|| {
32 serde_json::from_str(include_str!("../assets/lemmy/context.json")).expect("parse context")
36 pub struct VerifyUrlData(pub DbPool);
39 impl UrlVerifier for VerifyUrlData {
40 async fn verify(&self, url: &Url) -> Result<(), &'static str> {
41 let local_site_data = fetch_local_site_data(&self.0)
43 .expect("read local site data");
44 check_apub_id_valid(url, &local_site_data)?;
49 /// Checks if the ID is allowed for sending or receiving.
51 /// In particular, it checks for:
52 /// - federation being enabled (if its disabled, only local URLs are allowed)
53 /// - the correct scheme (either http or https)
54 /// - URL being in the allowlist (if it is active)
55 /// - URL not being in the blocklist (if it is active)
57 /// `use_strict_allowlist` should be true only when parsing a remote community, or when parsing a
58 /// post/comment in a local community.
59 #[tracing::instrument(skip(local_site_data))]
60 fn check_apub_id_valid(apub_id: &Url, local_site_data: &LocalSiteData) -> Result<(), &'static str> {
61 let domain = apub_id.domain().expect("apud id has domain").to_string();
66 .map(|l| l.federation_enabled)
69 return Err("Federation disabled");
72 if let Some(blocked) = local_site_data.blocked_instances.as_ref() {
73 if blocked.iter().any(|i| domain.eq(&i.domain)) {
74 return Err("Domain is blocked");
78 if let Some(allowed) = local_site_data.allowed_instances.as_ref() {
79 if !allowed.iter().any(|i| domain.eq(&i.domain)) {
80 return Err("Domain is not in allowlist");
88 pub(crate) struct LocalSiteData {
89 local_site: Option<LocalSite>,
90 allowed_instances: Option<Vec<Instance>>,
91 blocked_instances: Option<Vec<Instance>>,
94 pub(crate) async fn fetch_local_site_data(
96 ) -> Result<LocalSiteData, diesel::result::Error> {
97 // LocalSite may be missing
98 let local_site = LocalSite::read(pool).await.ok();
99 let allowed = Instance::allowlist(pool).await?;
100 let blocked = Instance::blocklist(pool).await?;
102 // These can return empty vectors, so convert them to options
103 let allowed_instances = (!allowed.is_empty()).then_some(allowed);
104 let blocked_instances = (!blocked.is_empty()).then_some(blocked);
113 #[tracing::instrument(skip(settings, local_site_data))]
114 pub(crate) fn check_apub_id_valid_with_strictness(
117 local_site_data: &LocalSiteData,
119 ) -> Result<(), LemmyError> {
120 let domain = apub_id.domain().expect("apud id has domain").to_string();
121 let local_instance = settings
122 .get_hostname_without_port()
123 .expect("local hostname is valid");
124 if domain == local_instance {
127 check_apub_id_valid(apub_id, local_site_data).map_err(LemmyError::from_message)?;
129 if let Some(allowed) = local_site_data.allowed_instances.as_ref() {
130 // Only check allowlist if this is a community
132 // need to allow this explicitly because apub receive might contain objects from our local
134 let mut allowed_and_local = allowed
136 .map(|i| i.domain.clone())
137 .collect::<Vec<String>>();
138 let local_instance = settings
139 .get_hostname_without_port()
140 .expect("local hostname is valid");
141 allowed_and_local.push(local_instance);
143 let domain = apub_id.domain().expect("apud id has domain").to_string();
144 if !allowed_and_local.contains(&domain) {
145 return Err(LemmyError::from_message(
146 "Federation forbidden by strict allowlist",
154 /// Store a sent or received activity in the database.
156 /// Stored activities are served over the HTTP endpoint `GET /activities/{type_}/{id}`. This also
157 /// ensures that the same activity cannot be received more than once.
158 #[tracing::instrument(skip(data, activity))]
159 async fn insert_activity<T>(
164 data: &Data<LemmyContext>,
165 ) -> Result<(), LemmyError>
169 let ap_id = ap_id.clone().into();
170 let form = ActivityInsertForm {
172 data: serde_json::to_value(activity)?,
174 sensitive: Some(sensitive),
177 Activity::create(data.pool(), &form).await?;
181 #[async_trait::async_trait]
182 pub trait SendActivity: Sync {
183 type Response: Sync + Send;
185 async fn send_activity(
187 _response: &Self::Response,
188 _context: &Data<LemmyContext>,
189 ) -> Result<(), LemmyError> {