Skip to main content

upub/traits/
fetch.rs

1use std::collections::BTreeMap;
2
3use apb::{Shortcuts, Activity, Actor, ActorMut, Base, Collection, CollectionPage, Object};
4use reqwest::{header::{ACCEPT, CONTENT_TYPE, USER_AGENT}, Method, Response};
5use sea_orm::{ActiveValue::Set, ColumnTrait, ConnectionTrait, DbErr, EntityTrait, IntoActiveModel, NotSet, QueryFilter, ActiveModelTrait};
6
7use super::{Addresser, Cloaker, Normalizer};
8use httpsign::HttpSignature;
9
10use crate::AP;
11
12#[derive(Debug, Clone)]
13pub enum Pull<T> {
14	Actor(T),
15	Activity(T),
16	Object(T),
17}
18
19#[derive(Debug, thiserror::Error)]
20pub enum RequestError {
21	#[error("dereferenced resource ({1:?}) doesn't match requested type ({0:?})")]
22	Mismatch(apb::ObjectType, apb::ObjectType),
23
24	#[error("error fetching resource: {0:?}")]
25	Reqwest(#[from] reqwest::Error),
26
27	#[error("fetch failed with status {0}: {1}")]
28	Fetch(reqwest::StatusCode, String),
29
30	#[error("database error while fetching resource: {0:?}")]
31	Database(#[from] sea_orm::DbErr),
32
33	#[error("dereferenced resource is malformed: {0:?}")]
34	Malformed(#[from] apb::FieldErr),
35
36	#[error("error normalizing resource: {0:?}")]
37	Normalization(#[from] crate::traits::normalize::NormalizerError),
38
39	#[error("too many redirects while resolving resource id, aborting")]
40	TooManyRedirects,
41
42	#[error("resource no longer exists")]
43	Tombstone,
44
45	#[error("request aborted due to configured policies")]
46	AbortedForPolicy,
47
48	#[error("error constructing http signature: {0:?}")]
49	HttpSignature(#[from] httpsign::HttpSignatureError),
50}
51
52impl RequestError {
53	fn mismatch(expected: apb::ObjectType, found: apb::ObjectType) -> Self {
54		RequestError::Mismatch(expected, found)
55	}
56}
57
58impl Pull<serde_json::Value> {
59	pub fn actor(self) -> Result<serde_json::Value, RequestError> {
60		match self {
61			Self::Actor(x) => Ok(x),
62			Self::Activity(x) => Err(RequestError::mismatch(apb::ObjectType::Actor(apb::ActorType::Person), x.object_type().unwrap_or(apb::ObjectType::Activity(apb::ActivityType::Activity)))),
63			Self::Object(x) => Err(RequestError::mismatch(apb::ObjectType::Actor(apb::ActorType::Person), x.object_type().unwrap_or(apb::ObjectType::Object))),
64		}
65	}
66
67	pub fn activity(self) -> Result<serde_json::Value, RequestError> {
68		match self {
69			Self::Actor(x) => Err(RequestError::mismatch(apb::ObjectType::Activity(apb::ActivityType::Activity), x.object_type().unwrap_or(apb::ObjectType::Actor(apb::ActorType::Person)))),
70			Self::Activity(x) => Ok(x),
71			Self::Object(x) => Err(RequestError::mismatch(apb::ObjectType::Activity(apb::ActivityType::Activity), x.object_type().unwrap_or(apb::ObjectType::Object))),
72		}
73	}
74
75	pub fn object(self) -> Result<serde_json::Value, RequestError> {
76		match self {
77			Self::Actor(x) => Err(RequestError::mismatch(apb::ObjectType::Object, x.object_type().unwrap_or(apb::ObjectType::Actor(apb::ActorType::Person)))),
78			Self::Activity(x) => Err(RequestError::mismatch(apb::ObjectType::Object, x.object_type().unwrap_or(apb::ObjectType::Activity(apb::ActivityType::Activity)))),
79			Self::Object(x) => Ok(x),
80		}
81	}
82}
83
84#[async_trait::async_trait]
85pub trait Fetcher {
86	async fn pull(&self, id: &str) -> Result<Pull<serde_json::Value>, RequestError> { self.pull_r(id, 0).await }
87	async fn pull_r(&self, id: &str, depth: u32) -> Result<Pull<serde_json::Value>, RequestError>;
88
89
90	async fn webfinger(&self, user: &str, host: &str) -> Result<Option<String>, RequestError>;
91
92	async fn fetch_domain(&self, domain: &str, tx: &impl ConnectionTrait) -> Result<crate::model::instance::Model, RequestError>;
93
94	async fn fetch_user(&self, id: &str, tx: &impl ConnectionTrait) -> Result<crate::model::actor::Model, RequestError>;
95	async fn resolve_user(&self, actor: serde_json::Value, tx: &impl ConnectionTrait) -> Result<crate::model::actor::ActiveModel, RequestError>;
96
97	async fn fetch_activity(&self, id: &str, tx: &impl ConnectionTrait) -> Result<crate::model::activity::Model, RequestError>;
98	async fn resolve_activity(&self, activity: serde_json::Value, tx: &impl ConnectionTrait) -> Result<crate::model::activity::Model, RequestError>;
99
100	async fn fetch_object(&self, id: &str, tx: &impl ConnectionTrait) -> Result<crate::model::object::Model, RequestError>;
101	async fn resolve_object(&self, object: serde_json::Value, tx: &impl ConnectionTrait) -> Result<crate::model::object::Model, RequestError>;
102
103	async fn fetch_thread(&self, id: &str, tx: &impl ConnectionTrait) -> Result<(), RequestError>;
104	async fn fetch_outbox(&self, id: &str, tx: &impl ConnectionTrait) -> Result<(), RequestError>;
105
106	fn client(domain: &str) -> reqwest::Client {
107		reqwest::Client::builder()
108			.user_agent(format!("upub+{} ({domain})", crate::VERSION))
109			.connect_timeout(std::time::Duration::from_secs(30)) // TODO may be cool to configure these
110			.read_timeout(std::time::Duration::from_secs(30)) // TODO may be cool to configure these
111			.timeout(std::time::Duration::from_secs(300)) // TODO may be cool to configure these
112			.build()
113			.expect("failed building http client, check system tls or resolver")
114	}
115
116	async fn request(
117		method: reqwest::Method,
118		url: &str,
119		payload: Option<&str>,
120		from: &str,
121		key: &str,
122		domain: &str,
123	) -> Result<Response, RequestError> {
124		let host = crate::Context::server(url);
125		let date = chrono::Utc::now().format("%a, %d %b %Y %H:%M:%S GMT").to_string(); // lmao @ "GMT"
126		let path = url.replace("https://", "").replace("http://", "").replace(&host, "");
127		let digest = httpsign::digest(payload.unwrap_or_default());
128
129		let headers = vec!["(request-target)", "host", "date", "digest"];
130		let headers_map : BTreeMap<String, String> = [
131			("host".to_string(), host.clone()),
132			("date".to_string(), date.clone()),
133			("digest".to_string(), digest.clone()),
134		].into();
135
136		let mut signer = HttpSignature::new(
137			format!("{from}#main-key"), // TODO don't hardcode #main-key
138			//"hs2019".to_string(), // TODO could we switch to this now?
139			"rsa-sha256".to_string(),
140			&headers,
141		);
142		
143		signer
144			.build_manually(&method.to_string().to_lowercase(), &path, headers_map)
145			.sign(key)?;
146
147		let response = Self::client(domain)
148			.request(method, url)
149			.header(ACCEPT, apb::jsonld::CONTENT_TYPE_LD_JSON_ACTIVITYPUB)
150			.header(CONTENT_TYPE, apb::jsonld::CONTENT_TYPE_LD_JSON_ACTIVITYPUB)
151			.header("Host", host.clone())
152			.header("Date", date.clone())
153			.header("Digest", digest)
154			.header("Signature", signer.header())
155			.body(payload.unwrap_or("").to_string())
156			.send()
157			.await?;
158
159		// TODO this is ugly but i want to see the raw response text when it's a failure
160		match response.error_for_status_ref() {
161			Ok(_) => Ok(response),
162			Err(e) =>
163				Err(RequestError::Fetch(
164					e.status().unwrap_or_default(),
165					response.text().await?,
166				)),
167		}
168	}
169}
170
171
172#[async_trait::async_trait]
173impl Fetcher for crate::Context {
174	async fn pull_r(&self, id: &str, depth: u32) -> Result<Pull<serde_json::Value>, RequestError> {
175		if crate::ext::is_blacklisted(id, &self.cfg().reject.fetch) {
176			return Err(RequestError::AbortedForPolicy);
177		}
178
179		tracing::debug!("fetching {id}");
180		// let _domain = self.fetch_domain(&crate::Context::server(id)).await?;
181
182		let document = Self::request(
183			Method::GET, id, None,
184			self.base(), self.pkey(), self.domain(),
185		)
186			.await?
187			.json::<serde_json::Value>()
188			.await?;
189
190		let doc_id = document.id()?;
191		if id != doc_id {
192			if depth >= self.cfg().security.max_id_redirects {
193				return Err(RequestError::TooManyRedirects);
194			}
195			return self.pull(&doc_id).await;
196		}
197
198		crate::downtime::unset(self.db(), &crate::Context::server(id)).await?;
199
200		match document.object_type()? {
201			apb::ObjectType::Collection(x) => Err(RequestError::mismatch(apb::ObjectType::Object, apb::ObjectType::Collection(x))),
202			apb::ObjectType::Tombstone => Err(RequestError::Tombstone),
203			apb::ObjectType::Activity(_) => Ok(Pull::Activity(document)),
204			apb::ObjectType::Actor(_) => Ok(Pull::Actor(document)),
205			_ => Ok(Pull::Object(document)),
206		}
207	}
208
209
210	async fn webfinger(&self, user: &str, host: &str) -> Result<Option<String>, RequestError> {
211		// TODO username:host is not guaranteed unique!!!! either return a Vec<String> or maybe specify
212		//      which kind of actor??
213		if let Some(usr) = crate::model::actor::Entity::find()
214			.filter(crate::model::actor::Column::PreferredUsername.eq(user))
215			.filter(crate::model::actor::Column::Domain.eq(host))
216			.one(self.db())
217			.await?
218		{
219			return Ok(Some(usr.id));
220		}
221
222		let subject = format!("acct:{user}@{host}");
223		let webfinger_uri = format!("https://{host}/.well-known/webfinger?resource={subject}");
224		let resource = reqwest::Client::new()
225			.get(webfinger_uri)
226			.header(ACCEPT, "application/jrd+json")
227			.header(USER_AGENT, format!("upub+{} ({})", crate::VERSION, self.domain()))
228			.send()
229			.await?
230			.json::<jrd::JsonResourceDescriptor>()
231			.await?;
232
233		if resource.subject != subject {
234			tracing::error!("webfinger result ({}) differs from expected subject ({})", resource.subject, subject);
235			return Ok(None);
236		}
237
238		for link in resource.links {
239			if link.rel == "self" {
240				if let Some(href) = link.href {
241					return Ok(Some(href));
242				}
243			}
244		}
245
246		if let Some(alias) = resource.aliases.into_iter().next() {
247			return Ok(Some(alias));
248		}
249
250		Ok(None)
251	}
252
253	async fn fetch_domain(&self, domain: &str, tx: &impl ConnectionTrait) -> Result<crate::model::instance::Model, RequestError> {
254		if let Some(x) = crate::model::instance::Entity::find_by_domain(domain).one(tx).await? {
255			return Ok(x); // already in db, easy
256		}
257
258		let mut instance_model = crate::model::instance::Model {
259			internal: 0,
260			domain: domain.to_string(),
261			name: None,
262			software: None,
263			down_since: None,
264			icon: None,
265			version: None,
266			users: None,
267			posts: None,
268			published: chrono::Utc::now(),
269			updated: chrono::Utc::now(),
270		};
271
272		if let Ok(res) = Self::request(
273			Method::GET, &format!("https://{domain}"), None,
274			self.base(), self.pkey(), self.domain(),
275		).await {
276			if let Ok(actor) = res.json::<serde_json::Value>().await {
277				if let Ok(name) = actor.name() {
278					instance_model.name = Some(name.to_string());
279				}
280				if let Ok(icon) = actor.icon().id() {
281					instance_model.icon = Some(icon.to_string());
282				}
283			}
284		}
285
286		if let Ok(nodeinfo) = crate::model::instance::Entity::nodeinfo(domain).await {
287			instance_model.software = Some(nodeinfo.software.name);
288			instance_model.version = nodeinfo.software.version;
289			instance_model.users = nodeinfo.usage.users.and_then(|x| x.total);
290			instance_model.posts = nodeinfo.usage.local_posts;
291		}
292
293		let mut active_model = instance_model.clone().into_active_model();
294		active_model.internal = NotSet;
295		crate::model::instance::Entity::insert(active_model).exec(tx).await?;
296		let internal = crate::model::instance::Entity::domain_to_internal(domain, tx)
297			.await?
298			.ok_or_else(|| DbErr::RecordNotFound(domain.to_string()))?;
299		instance_model.internal = internal;
300
301		Ok(instance_model)
302	}
303
304	async fn resolve_user(&self, mut document: serde_json::Value, tx: &impl ConnectionTrait) -> Result<crate::model::actor::ActiveModel, RequestError> {
305		let id = document.id()?.to_string();
306
307		let _domain = self.fetch_domain(&crate::Context::server(&id), tx).await?;
308
309		// TODO try fetching these numbers from audience/generator fields to avoid making 2 more GETs every time
310		if document.followers_count().is_err() {
311			if let Ok(followers) = document.followers().resolve(self).await {
312				if let Ok(total) = followers.total_items() {
313					document = document.set_followers_count(Some(total));
314				}
315			}
316		}
317
318		if document.following_count().is_err() {
319			if let Ok(following) = document.following().resolve(self).await {
320				if let Ok(total) = following.total_items() {
321					document = document.set_following_count(Some(total));
322				}
323			}
324		}
325
326		if document.statuses_count().is_err() {
327			if let Ok(outbox) = document.outbox().resolve(self).await {
328				if let Ok(total) = outbox.total_items() {
329					document = document.set_statuses_count(Some(total));
330				}
331			}
332		}
333
334		let mut user_model = AP::actor_q(&document, None)?;
335
336		// cloak remote images
337		if let Set(Some(ref image)) = user_model.image {
338			if !image.starts_with(self.base()) {
339				user_model.image = Set(Some(self.cloaked(image)));
340			}
341		}
342
343		if let Set(Some(ref icon)) = user_model.icon {
344			if !icon.starts_with(self.base()) {
345				user_model.icon = Set(Some(self.cloaked(icon)));
346			}
347		}
348
349		Ok(user_model)
350	}
351
352	async fn fetch_user(&self, id: &str, tx: &impl ConnectionTrait) -> Result<crate::model::actor::Model, RequestError> {
353		if let Some(x) = crate::model::actor::Entity::find_by_ap_id(id).one(tx).await? {
354			return Ok(x); // already in db, easy
355		}
356
357		let document = self.pull(id).await?.actor()?;
358
359		if document.id()? != id {
360			if let Some(x) = crate::model::actor::Entity::find_by_ap_id(&document.id()?).one(tx).await? {
361				return Ok(x); // already in db but we had to follow the "pretty" url, mehh
362			}
363		}
364
365		let active_model = self.resolve_user(document, tx).await?;
366
367		// TODO this may fail: while fetching, remote server may fetch our service actor.
368		//      if it does so with http signature, we will fetch that actor in background
369		//      meaning that, once we reach here, it's already inserted and returns an UNIQUE error
370		crate::model::actor::Entity::insert(active_model).exec(tx).await?;
371		
372		// TODO fetch it back to get the internal id
373		Ok(
374			crate::model::actor::Entity::find_by_ap_id(id)
375				.one(tx)
376				.await?
377				.ok_or_else(|| DbErr::RecordNotFound(id.to_string()))?
378		)
379	}
380
381	async fn fetch_activity(&self, id: &str, tx: &impl ConnectionTrait) -> Result<crate::model::activity::Model, RequestError> {
382		if let Some(x) = crate::model::activity::Entity::find_by_ap_id(id).one(tx).await? {
383			return Ok(x); // already in db, easy
384		}
385
386		let activity = self.pull(id).await?.activity()?;
387
388		self.resolve_activity(activity, tx).await
389	}
390
391	async fn resolve_activity(&self, activity: serde_json::Value, tx: &impl ConnectionTrait) -> Result<crate::model::activity::Model, RequestError> {
392		let _domain = self.fetch_domain(&crate::Context::server(&activity.id()?), tx).await?;
393
394		if let Ok(activity_actor) = activity.actor().id() {
395			if let Err(e) = self.fetch_user(&activity_actor, tx).await {
396				tracing::warn!("could not get actor of fetched activity: {e}");
397			}
398		}
399
400		if let Ok(activity_object) = activity.object().id() {
401			if let Err(e) = self.fetch_object(&activity_object, tx).await {
402				tracing::warn!("could not get object of fetched activity: {e}");
403			}
404		}
405
406		let activity_model = self.insert_activity(activity, tx).await?;
407		self.address(Some(&activity_model), None, tx).await?;
408
409		Ok(activity_model)
410	}
411
412	async fn fetch_thread(&self, id: &str, tx: &impl ConnectionTrait) -> Result<(), RequestError> {
413		let object = self.pull(id).await?.object()?;
414
415		// also update object stats since we're pulling it again
416		let model = self.fetch_object(id, tx).await?;
417		let mut active = model.clone().into_active_model();
418		let mut changed = false;
419
420		let new_like_count = object.likes_count().unwrap_or_default();
421		if new_like_count > model.likes {
422			active.likes = Set(new_like_count);
423			changed = true;
424		}
425
426		let new_share_count = object.shares_count().unwrap_or_default();
427		if new_share_count > model.announces {
428			active.announces = Set(new_share_count);
429			changed = true;
430		}
431
432		if changed {
433			active.update(tx).await?;
434		}
435
436		// crawl replies collection
437		let replies = object.replies().resolve(self).await?;
438
439		// first flat out items in bare collection
440		for obj in replies.items().flat() {
441			if let Err(e) = self.fetch_object(&obj.id()?, tx).await {
442				tracing::warn!("error fetching reply: {e}");
443			}
444		}
445
446		for obj in replies.ordered_items().flat() {
447			if let Err(e) = self.fetch_object(&obj.id()?, tx).await {
448				tracing::warn!("error fetching reply: {e}");
449			}
450		}
451
452		// then try cycling thru pagination
453		let mut page;
454		let mut next = replies.first();
455
456		loop {
457			page = next.resolve(self).await?;
458
459			// fix for mastodon: at some point it introduces ?only_other_accounts=true and then returns a
460			// collection, not a page anymore ???
461			if matches!(page.object_type()?, apb::ObjectType::Collection(apb::CollectionType::Collection)) {
462				page = page.first().into_inner()?;
463			}
464
465			// TODO parallelize these
466
467			for obj in page.items().flat() {
468				if let Err(e) = self.fetch_object(&obj.id()?, tx).await {
469					tracing::warn!("error fetching reply: {e}");
470				}
471			}
472
473			for obj in page.ordered_items().flat() {
474				if let Err(e) = self.fetch_object(&obj.id()?, tx).await {
475					tracing::warn!("error fetching reply: {e}");
476				}
477			}
478
479			next = page.next();
480			if next.is_empty() { break };
481		}
482
483		Ok(())
484	}
485
486	async fn fetch_outbox(&self, id: &str, tx: &impl ConnectionTrait) -> Result<(), RequestError> {
487		let actor = self.pull(id).await?.actor()?;
488		let outbox = actor
489			.outbox().resolve(self).await?
490			.first().resolve(self).await?;
491
492		// TODO parallelize these
493
494		// TODO we're getting inner objects because
495		//  1) fetching activities makes it look like we're following this actor: is this desirable?
496		//  2) fetching activities doesn't properly merge addressing, so we get orphan objects twice
497		for node in outbox.ordered_items().flat() {
498			if let Ok(activity) = node.into_inner() {
499				if matches!(activity.activity_type(), Ok(apb::ActivityType::Create)) {
500					if let Ok(oid) = activity.object().id() {
501						self.fetch_object(&oid, tx).await?;
502					}
503				}
504			}
505		}
506
507		Ok(())
508	}
509
510	async fn fetch_object(&self, id: &str, tx: &impl ConnectionTrait) -> Result<crate::model::object::Model, RequestError> {
511		fetch_object_r(self, id, 0, tx).await
512	}
513
514	async fn resolve_object(&self, object: serde_json::Value, tx: &impl ConnectionTrait) -> Result<crate::model::object::Model, RequestError> {
515		resolve_object_r(self, object, 0, tx).await
516	}
517}
518
519#[async_recursion::async_recursion]
520async fn fetch_object_r(ctx: &crate::Context, id: &str, depth: u32, tx: &impl ConnectionTrait) -> Result<crate::model::object::Model, RequestError> {
521	if let Some(x) = crate::model::object::Entity::find_by_ap_id(id).one(tx).await? {
522		return Ok(x); // already in db, easy
523	}
524
525	let object = ctx.pull(id).await?.object()?;
526
527	if object.id()? != id {
528		if let Some(x) = crate::model::object::Entity::find_by_ap_id(&object.id()?).one(tx).await? {
529			return Ok(x); // already in db but we had to follow the "pretty" url, mehh
530		}
531	}
532
533	resolve_object_r(ctx, object, depth, tx).await
534}
535
536async fn resolve_object_r(ctx: &crate::Context, object: serde_json::Value, depth: u32, tx: &impl ConnectionTrait) -> Result<crate::model::object::Model, RequestError> {
537	let id = object.id()?.to_string();
538
539	if let Ok(oid) = object.id() {
540		if oid != id {
541			if let Some(x) = crate::model::object::Entity::find_by_ap_id(&oid).one(tx).await? {
542				return Ok(x); // already in db, but with id different that given url
543			}
544		}
545	}
546
547	if let Ok(attributed_to) = object.attributed_to().id() {
548		if let Err(e) = ctx.fetch_user(&attributed_to, tx).await {
549			tracing::warn!("could not get actor of fetched object: {e}");
550		}
551	}
552
553	if let Ok(reply) = object.in_reply_to().id() {
554		if depth <= ctx.cfg().security.thread_crawl_depth {
555			fetch_object_r(ctx, &reply, depth + 1, tx).await?;
556		} else {
557			tracing::warn!("thread deeper than {}, giving up fetching more replies", ctx.cfg().security.thread_crawl_depth);
558		}
559	}
560
561	let object_model = ctx.insert_object(object, tx).await?;
562	ctx.address(None, Some(&object_model), tx).await?;
563
564	Ok(object_model)
565}
566
567#[allow(async_fn_in_trait)]
568pub trait Dereferenceable<T> : Sync + Send {
569	async fn resolve(self, ctx: &crate::Context) -> Result<T, RequestError>;
570}
571
572impl Dereferenceable<serde_json::Value> for apb::Node<serde_json::Value> {
573	async fn resolve(self, ctx: &crate::Context) -> Result<serde_json::Value, RequestError> {
574		match self {
575			apb::Node::Link(uri) => {
576				let href = uri.href()?;
577				if crate::ext::is_blacklisted(&href, &ctx.cfg().reject.fetch) {
578					return Err(RequestError::AbortedForPolicy);
579				}
580				tracing::info!("dereferencing {href}");
581				let res = crate::Context::request(Method::GET, &href, None, ctx.base(), ctx.pkey(), ctx.domain())
582					.await?
583					.json::<serde_json::Value>()
584					.await?;
585				Ok(res)
586			},
587			apb::Node::Object(x) => Ok(*x),
588			apb::Node::Empty => Err(RequestError::Tombstone),
589			apb::Node::Array(_) => Err(RequestError::Malformed(apb::FieldErr("id"))), // TODO weird!!
590		}
591	}
592}