| 
									
										
										
										
											2023-05-17 13:13:30 -04:00
										 |  |  | 'use strict'; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-06-19 17:29:22 -04:00
										 |  |  | const nconf = require('nconf'); | 
					
						
							| 
									
										
										
										
											2024-01-05 22:45:33 -05:00
										 |  |  | const winston = require('winston'); | 
					
						
							| 
									
										
										
										
											2024-04-11 10:39:51 -04:00
										 |  |  | const { createHash, createSign, createVerify, getHashes } = require('crypto'); | 
					
						
							| 
									
										
										
										
											2024-06-04 14:18:14 -04:00
										 |  |  | const { CronJob } = require('cron'); | 
					
						
							| 
									
										
										
										
											2023-05-17 13:13:30 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-12-19 14:33:38 -05:00
										 |  |  | const request = require('../request'); | 
					
						
							| 
									
										
										
										
											2023-05-29 17:42:44 -04:00
										 |  |  | const db = require('../database'); | 
					
						
							| 
									
										
										
										
											2024-02-26 16:12:40 -05:00
										 |  |  | const meta = require('../meta'); | 
					
						
							| 
									
										
										
										
											2025-03-24 14:29:26 -04:00
										 |  |  | const categories = require('../categories'); | 
					
						
							| 
									
										
										
										
											2024-10-23 11:28:55 -04:00
										 |  |  | const posts = require('../posts'); | 
					
						
							|  |  |  | const messaging = require('../messaging'); | 
					
						
							| 
									
										
										
										
											2023-06-19 17:29:22 -04:00
										 |  |  | const user = require('../user'); | 
					
						
							| 
									
										
										
										
											2024-01-22 16:18:49 -05:00
										 |  |  | const utils = require('../utils'); | 
					
						
							| 
									
										
										
										
											2023-06-23 14:59:47 -04:00
										 |  |  | const ttl = require('../cache/ttl'); | 
					
						
							| 
									
										
										
										
											2024-05-06 22:49:31 +02:00
										 |  |  | const lru = require('../cache/lru'); | 
					
						
							| 
									
										
										
										
											2024-05-06 22:52:48 +02:00
										 |  |  | const batch = require('../batch'); | 
					
						
							| 
									
										
										
										
											2024-05-06 23:16:58 +02:00
										 |  |  | const pubsub = require('../pubsub'); | 
					
						
							| 
									
										
										
										
											2024-05-14 12:06:59 -04:00
										 |  |  | const analytics = require('../analytics'); | 
					
						
							| 
									
										
										
										
											2023-05-17 13:13:30 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-06-04 12:31:13 -04:00
										 |  |  | const requestCache = ttl({ | 
					
						
							|  |  |  | 	max: 5000, | 
					
						
							|  |  |  | 	ttl: 1000 * 60 * 5, // 5 minutes
 | 
					
						
							|  |  |  | }); | 
					
						
							| 
									
										
										
										
											2024-10-23 15:25:16 -04:00
										 |  |  | const probeCache = ttl({ | 
					
						
							|  |  |  | 	max: 500, | 
					
						
							|  |  |  | 	ttl: 1000 * 60 * 60, // 1 hour
 | 
					
						
							|  |  |  | }); | 
					
						
							| 
									
										
										
										
											2024-07-03 22:35:46 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-05-17 13:13:30 -04:00
										 |  |  | const ActivityPub = module.exports; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-01-24 11:44:10 -05:00
										 |  |  | ActivityPub._constants = Object.freeze({ | 
					
						
							| 
									
										
										
										
											2024-02-26 11:39:32 -05:00
										 |  |  | 	uid: -2, | 
					
						
							| 
									
										
										
										
											2024-01-24 11:44:10 -05:00
										 |  |  | 	publicAddress: 'https://www.w3.org/ns/activitystreams#Public', | 
					
						
							| 
									
										
										
										
											2024-04-08 20:06:26 +02:00
										 |  |  | 	acceptableTypes: [ | 
					
						
							|  |  |  | 		'application/activity+json', | 
					
						
							|  |  |  | 		'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', | 
					
						
							|  |  |  | 	], | 
					
						
							| 
									
										
										
										
											2024-04-14 02:42:30 +02:00
										 |  |  | 	acceptedPostTypes: [ | 
					
						
							| 
									
										
										
										
											2025-02-26 13:55:39 -05:00
										 |  |  | 		'Note', 'Page', 'Article', 'Question', 'Video', | 
					
						
							| 
									
										
										
										
											2024-04-14 02:42:30 +02:00
										 |  |  | 	], | 
					
						
							| 
									
										
										
										
											2025-03-13 15:50:44 -04:00
										 |  |  | 	acceptableActorTypes: new Set(['Application', 'Organization', 'Person', 'Service']), | 
					
						
							|  |  |  | 	acceptableGroupTypes: new Set(['Group']), | 
					
						
							| 
									
										
										
										
											2024-09-19 14:52:05 -04:00
										 |  |  | 	requiredActorProps: ['inbox', 'outbox'], | 
					
						
							| 
									
										
										
										
											2024-04-25 20:05:53 +02:00
										 |  |  | 	acceptedProtocols: ['https', ...(process.env.CI === 'true' ? ['http'] : [])], | 
					
						
							| 
									
										
										
										
											2024-11-26 14:18:42 -05:00
										 |  |  | 	acceptable: { | 
					
						
							|  |  |  | 		customFields: new Set(['PropertyValue', 'Link', 'Note']), | 
					
						
							| 
									
										
										
										
											2025-01-30 16:41:52 -05:00
										 |  |  | 		contextTypes: new Set(['Collection', 'CollectionPage', 'OrderedCollection', 'OrderedCollectionPage']), | 
					
						
							| 
									
										
										
										
											2024-11-26 14:18:42 -05:00
										 |  |  | 	}, | 
					
						
							| 
									
										
										
										
											2024-01-24 11:44:10 -05:00
										 |  |  | }); | 
					
						
							| 
									
										
										
										
											2024-02-21 10:26:26 -05:00
										 |  |  | ActivityPub._cache = requestCache; | 
					
						
							| 
									
										
										
										
											2025-02-28 13:56:33 -05:00
										 |  |  | ActivityPub._sent = new Map(); // used only in local tests
 | 
					
						
							| 
									
										
										
										
											2024-01-24 11:44:10 -05:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-06-02 14:22:43 -04:00
										 |  |  | ActivityPub.helpers = require('./helpers'); | 
					
						
							| 
									
										
										
										
											2023-06-28 14:59:39 -04:00
										 |  |  | ActivityPub.inbox = require('./inbox'); | 
					
						
							| 
									
										
										
										
											2024-01-10 14:19:57 -05:00
										 |  |  | ActivityPub.mocks = require('./mocks'); | 
					
						
							| 
									
										
										
										
											2024-01-11 10:05:02 -05:00
										 |  |  | ActivityPub.notes = require('./notes'); | 
					
						
							| 
									
										
										
										
											2024-06-28 12:54:32 -04:00
										 |  |  | ActivityPub.contexts = require('./contexts'); | 
					
						
							| 
									
										
										
										
											2024-01-26 15:10:35 -05:00
										 |  |  | ActivityPub.actors = require('./actors'); | 
					
						
							| 
									
										
										
										
											2024-06-17 15:50:27 -04:00
										 |  |  | ActivityPub.instances = require('./instances'); | 
					
						
							| 
									
										
										
										
											2025-02-07 08:21:41 -05:00
										 |  |  | ActivityPub.feps = require('./feps'); | 
					
						
							| 
									
										
										
										
											2023-06-02 14:22:43 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-06-04 14:18:14 -04:00
										 |  |  | ActivityPub.startJobs = () => { | 
					
						
							| 
									
										
										
										
											2024-10-12 22:49:24 -04:00
										 |  |  | 	ActivityPub.helpers.log('[activitypub/jobs] Registering jobs.'); | 
					
						
							| 
									
										
										
										
											2024-06-10 19:24:06 -04:00
										 |  |  | 	new CronJob('0 0 * * *', async () => { | 
					
						
							| 
									
										
										
										
											2025-02-19 20:09:31 -05:00
										 |  |  | 		if (!meta.config.activitypubEnabled) { | 
					
						
							|  |  |  | 			return; | 
					
						
							|  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2024-06-10 19:24:06 -04:00
										 |  |  | 		try { | 
					
						
							|  |  |  | 			await ActivityPub.notes.prune(); | 
					
						
							| 
									
										
										
										
											2025-02-18 12:06:02 -05:00
										 |  |  | 			await db.sortedSetsRemoveRangeByScore(['activities:datetime'], '-inf', Date.now() - 604800000); | 
					
						
							| 
									
										
										
										
											2024-06-10 19:24:06 -04:00
										 |  |  | 		} catch (err) { | 
					
						
							|  |  |  | 			winston.error(err.stack); | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	}, null, true, null, null, false); // change last argument to true for debugging
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-02-11 10:39:24 -05:00
										 |  |  | 	new CronJob('*/30 * * * *', async () => { | 
					
						
							| 
									
										
										
										
											2025-02-19 20:09:31 -05:00
										 |  |  | 		if (!meta.config.activitypubEnabled) { | 
					
						
							|  |  |  | 			return; | 
					
						
							|  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2024-06-10 19:24:06 -04:00
										 |  |  | 		try { | 
					
						
							|  |  |  | 			await ActivityPub.actors.prune(); | 
					
						
							|  |  |  | 		} catch (err) { | 
					
						
							|  |  |  | 			winston.error(err.stack); | 
					
						
							|  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2024-06-10 19:27:03 -04:00
										 |  |  | 	}, null, true, null, null, false); // change last argument to true for debugging
 | 
					
						
							| 
									
										
										
										
											2024-06-04 14:18:14 -04:00
										 |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-21 10:58:20 -05:00
										 |  |  | ActivityPub.resolveId = async (uid, id) => { | 
					
						
							|  |  |  | 	try { | 
					
						
							|  |  |  | 		const query = new URL(id); | 
					
						
							|  |  |  | 		({ id } = await ActivityPub.get('uid', uid, id)); | 
					
						
							|  |  |  | 		const response = new URL(id); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		if (query.host !== response.host) { | 
					
						
							|  |  |  | 			winston.warn(`[activitypub/resolveId] id resolution domain mismatch: ${query.href} != ${response.href}`); | 
					
						
							|  |  |  | 			return null; | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		return id; | 
					
						
							|  |  |  | 	} catch (e) { | 
					
						
							|  |  |  | 		return null; | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-01-26 15:10:35 -05:00
										 |  |  | ActivityPub.resolveInboxes = async (ids) => { | 
					
						
							| 
									
										
										
										
											2024-01-24 11:44:10 -05:00
										 |  |  | 	const inboxes = new Set(); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-26 16:12:40 -05:00
										 |  |  | 	if (!meta.config.activitypubAllowLoopback) { | 
					
						
							|  |  |  | 		ids = ids.filter((id) => { | 
					
						
							|  |  |  | 			const { hostname } = new URL(id); | 
					
						
							| 
									
										
										
										
											2024-07-05 12:14:16 -04:00
										 |  |  | 			return hostname !== nconf.get('url_parsed').hostname; | 
					
						
							| 
									
										
										
										
											2024-02-26 16:12:40 -05:00
										 |  |  | 		}); | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-01-26 21:39:20 -05:00
										 |  |  | 	await ActivityPub.actors.assert(ids); | 
					
						
							| 
									
										
										
										
											2025-03-24 14:29:26 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	// Remove non-asserted targets
 | 
					
						
							|  |  |  | 	const exists = await db.isSortedSetMembers('usersRemote:lastCrawled', ids); | 
					
						
							|  |  |  | 	ids = ids.filter((_, idx) => exists[idx]); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-03 22:35:46 -04:00
										 |  |  | 	await batch.processArray(ids, async (currentIds) => { | 
					
						
							| 
									
										
										
										
											2025-03-25 10:20:50 -04:00
										 |  |  | 		const isCategory = await db.exists(currentIds.map(id => `categoryRemote:${id}`)); | 
					
						
							|  |  |  | 		const [cids, uids] = currentIds.reduce(([cids, uids], id, idx) => { | 
					
						
							|  |  |  | 			const array = isCategory[idx] ? cids : uids; | 
					
						
							|  |  |  | 			array.push(id); | 
					
						
							|  |  |  | 			return [cids, uids]; | 
					
						
							|  |  |  | 		}, [[], []]); | 
					
						
							|  |  |  | 		const categoryData = await categories.getCategoriesFields(cids, ['inbox', 'sharedInbox']); | 
					
						
							|  |  |  | 		const userData = await user.getUsersFields(uids, ['inbox', 'sharedInbox']); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		currentIds.forEach((id) => { | 
					
						
							|  |  |  | 			if (cids.includes(id)) { | 
					
						
							|  |  |  | 				const data = categoryData[cids.indexOf(id)]; | 
					
						
							|  |  |  | 				inboxes.add(data.sharedInbox || data.inbox); | 
					
						
							|  |  |  | 			} else if (uids.includes(id)) { | 
					
						
							|  |  |  | 				const data = userData[uids.indexOf(id)]; | 
					
						
							|  |  |  | 				inboxes.add(data.sharedInbox || data.inbox); | 
					
						
							| 
									
										
										
										
											2024-07-03 22:35:46 -04:00
										 |  |  | 			} | 
					
						
							|  |  |  | 		}); | 
					
						
							|  |  |  | 	}, { | 
					
						
							|  |  |  | 		batch: 500, | 
					
						
							|  |  |  | 	}); | 
					
						
							| 
									
										
										
										
											2024-01-24 11:44:10 -05:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	return Array.from(inboxes); | 
					
						
							|  |  |  | }; | 
					
						
							| 
									
										
										
										
											2023-06-23 14:59:47 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-05 16:57:17 -05:00
										 |  |  | ActivityPub.getPublicKey = async (type, id) => { | 
					
						
							| 
									
										
										
										
											2023-05-17 13:13:30 -04:00
										 |  |  | 	let publicKey; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	try { | 
					
						
							| 
									
										
										
										
											2024-02-06 10:40:46 -05:00
										 |  |  | 		({ publicKey } = await db.getObject(`${type}:${id}:keys`)); | 
					
						
							| 
									
										
										
										
											2023-05-17 13:13:30 -04:00
										 |  |  | 	} catch (e) { | 
					
						
							| 
									
										
										
										
											2024-02-05 16:57:17 -05:00
										 |  |  | 		({ publicKey } = await ActivityPub.helpers.generateKeys(type, id)); | 
					
						
							| 
									
										
										
										
											2023-05-17 13:13:30 -04:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return publicKey; | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-05 16:57:17 -05:00
										 |  |  | ActivityPub.getPrivateKey = async (type, id) => { | 
					
						
							|  |  |  | 	// Sanity checking
 | 
					
						
							|  |  |  | 	if (!['cid', 'uid'].includes(type) || !utils.isNumber(id) || parseInt(id, 10) < 0) { | 
					
						
							|  |  |  | 		throw new Error('[[error:invalid-data]]'); | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	id = parseInt(id, 10); | 
					
						
							| 
									
										
										
										
											2023-06-19 17:29:22 -04:00
										 |  |  | 	let privateKey; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	try { | 
					
						
							| 
									
										
										
										
											2024-02-05 16:57:17 -05:00
										 |  |  | 		({ privateKey } = await db.getObject(`${type}:${id}:keys`)); | 
					
						
							| 
									
										
										
										
											2023-06-19 17:29:22 -04:00
										 |  |  | 	} catch (e) { | 
					
						
							| 
									
										
										
										
											2024-02-05 16:57:17 -05:00
										 |  |  | 		({ privateKey } = await ActivityPub.helpers.generateKeys(type, id)); | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	let keyId; | 
					
						
							|  |  |  | 	if (type === 'uid') { | 
					
						
							|  |  |  | 		keyId = `${nconf.get('url')}${id > 0 ? `/uid/${id}` : '/actor'}#key`; | 
					
						
							|  |  |  | 	} else { | 
					
						
							|  |  |  | 		keyId = `${nconf.get('url')}/category/${id}#key`; | 
					
						
							| 
									
										
										
										
											2023-06-19 17:29:22 -04:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-05 16:57:17 -05:00
										 |  |  | 	return { key: privateKey, keyId }; | 
					
						
							| 
									
										
										
										
											2023-06-19 17:29:22 -04:00
										 |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ActivityPub.fetchPublicKey = async (uri) => { | 
					
						
							|  |  |  | 	// Used for retrieving the public key from the passed-in keyId uri
 | 
					
						
							| 
									
										
										
										
											2024-04-09 19:27:35 +02:00
										 |  |  | 	const body = await ActivityPub.get('uid', 0, uri); | 
					
						
							| 
									
										
										
										
											2023-05-17 13:13:30 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-09 19:27:35 +02:00
										 |  |  | 	if (!body.hasOwnProperty('publicKey')) { | 
					
						
							| 
									
										
										
										
											2024-01-18 15:21:46 -05:00
										 |  |  | 		throw new Error('[[error:activitypub.pubKey-not-found]]'); | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-12-19 14:33:38 -05:00
										 |  |  | 	return body.publicKey; | 
					
						
							| 
									
										
										
										
											2023-06-19 17:29:22 -04:00
										 |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-05 16:57:17 -05:00
										 |  |  | ActivityPub.sign = async ({ key, keyId }, url, payload) => { | 
					
						
							| 
									
										
										
										
											2023-06-19 17:29:22 -04:00
										 |  |  | 	// Returns string for use in 'Signature' header
 | 
					
						
							|  |  |  | 	const { host, pathname } = new URL(url); | 
					
						
							|  |  |  | 	const date = new Date().toUTCString(); | 
					
						
							|  |  |  | 	let digest = null; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	let headers = '(request-target) host date'; | 
					
						
							|  |  |  | 	let signed_string = `(request-target): ${payload ? 'post' : 'get'} ${pathname}\nhost: ${host}\ndate: ${date}`; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Calculate payload hash if payload present
 | 
					
						
							|  |  |  | 	if (payload) { | 
					
						
							|  |  |  | 		const payloadHash = createHash('sha256'); | 
					
						
							|  |  |  | 		payloadHash.update(JSON.stringify(payload)); | 
					
						
							| 
									
										
										
										
											2024-04-06 02:06:41 +02:00
										 |  |  | 		digest = `SHA-256=${payloadHash.digest('base64')}`; | 
					
						
							| 
									
										
										
										
											2023-06-19 17:29:22 -04:00
										 |  |  | 		headers += ' digest'; | 
					
						
							|  |  |  | 		signed_string += `\ndigest: ${digest}`; | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Sign string using private key
 | 
					
						
							|  |  |  | 	let signature = createSign('sha256'); | 
					
						
							| 
									
										
										
										
											2023-12-22 12:58:46 -05:00
										 |  |  | 	signature.update(signed_string); | 
					
						
							| 
									
										
										
										
											2023-06-19 17:29:22 -04:00
										 |  |  | 	signature.end(); | 
					
						
							| 
									
										
										
										
											2023-12-22 12:58:46 -05:00
										 |  |  | 	signature = signature.sign(key, 'base64'); | 
					
						
							| 
									
										
										
										
											2023-06-19 17:29:22 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	// Construct signature header
 | 
					
						
							|  |  |  | 	return { | 
					
						
							|  |  |  | 		date, | 
					
						
							|  |  |  | 		digest, | 
					
						
							| 
									
										
										
										
											2024-04-11 10:39:51 -04:00
										 |  |  | 		signature: `keyId="${keyId}",headers="${headers}",signature="${signature}",algorithm="hs2019"`, | 
					
						
							| 
									
										
										
										
											2023-06-19 17:29:22 -04:00
										 |  |  | 	}; | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ActivityPub.verify = async (req) => { | 
					
						
							| 
									
										
										
										
											2024-10-12 22:49:24 -04:00
										 |  |  | 	ActivityPub.helpers.log('[activitypub/verify] Starting signature verification...'); | 
					
						
							| 
									
										
										
										
											2024-01-19 11:43:21 -05:00
										 |  |  | 	if (!req.headers.hasOwnProperty('signature')) { | 
					
						
							| 
									
										
										
										
											2024-10-12 22:49:24 -04:00
										 |  |  | 		ActivityPub.helpers.log('[activitypub/verify]   Failed, no signature header.'); | 
					
						
							| 
									
										
										
										
											2024-01-19 11:43:21 -05:00
										 |  |  | 		return false; | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-05-11 22:31:00 -04:00
										 |  |  | 	// Verify the signature string via public key
 | 
					
						
							|  |  |  | 	try { | 
					
						
							|  |  |  | 		// Break the signature apart
 | 
					
						
							|  |  |  | 		let { keyId, headers, signature, algorithm, created, expires } = req.headers.signature.split(',').reduce((memo, cur) => { | 
					
						
							|  |  |  | 			const split = cur.split('="'); | 
					
						
							|  |  |  | 			const key = split.shift(); | 
					
						
							|  |  |  | 			const value = split.join('="'); | 
					
						
							|  |  |  | 			memo[key] = value.slice(0, -1); | 
					
						
							|  |  |  | 			return memo; | 
					
						
							|  |  |  | 		}, {}); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		const acceptableHashes = getHashes(); | 
					
						
							|  |  |  | 		if (algorithm === 'hs2019' || !acceptableHashes.includes(algorithm)) { | 
					
						
							|  |  |  | 			algorithm = 'sha256'; | 
					
						
							|  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2024-04-11 15:20:59 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-05-11 22:31:00 -04:00
										 |  |  | 		// Re-construct signature string
 | 
					
						
							|  |  |  | 		const signed_string = headers.split(' ').reduce((memo, cur) => { | 
					
						
							|  |  |  | 			switch (cur) { | 
					
						
							|  |  |  | 				case '(request-target)': { | 
					
						
							|  |  |  | 					memo.push(`${cur}: ${String(req.method).toLowerCase()} ${req.baseUrl}${req.path}`); | 
					
						
							|  |  |  | 					break; | 
					
						
							|  |  |  | 				} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				case '(created)': { | 
					
						
							|  |  |  | 					memo.push(`${cur}: ${created}`); | 
					
						
							|  |  |  | 					break; | 
					
						
							|  |  |  | 				} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				case '(expires)': { | 
					
						
							|  |  |  | 					memo.push(`${cur}: ${expires}`); | 
					
						
							|  |  |  | 					break; | 
					
						
							|  |  |  | 				} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				default: { | 
					
						
							|  |  |  | 					memo.push(`${cur}: ${req.headers[cur]}`); | 
					
						
							|  |  |  | 					break; | 
					
						
							|  |  |  | 				} | 
					
						
							| 
									
										
										
										
											2024-04-11 15:20:59 -04:00
										 |  |  | 			} | 
					
						
							| 
									
										
										
										
											2023-06-19 17:29:22 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-05-11 22:31:00 -04:00
										 |  |  | 			return memo; | 
					
						
							|  |  |  | 		}, []).join('\n'); | 
					
						
							| 
									
										
										
										
											2023-06-19 17:29:22 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-01-18 15:21:46 -05:00
										 |  |  | 		// Retrieve public key from remote instance
 | 
					
						
							| 
									
										
										
										
											2024-10-12 22:49:24 -04:00
										 |  |  | 		ActivityPub.helpers.log(`[activitypub/verify] Retrieving pubkey for ${keyId}`); | 
					
						
							| 
									
										
										
										
											2024-01-18 15:21:46 -05:00
										 |  |  | 		const { publicKeyPem } = await ActivityPub.fetchPublicKey(keyId); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-06-19 17:29:22 -04:00
										 |  |  | 		const verify = createVerify('sha256'); | 
					
						
							| 
									
										
										
										
											2023-12-22 12:58:46 -05:00
										 |  |  | 		verify.update(signed_string); | 
					
						
							| 
									
										
										
										
											2023-06-19 17:29:22 -04:00
										 |  |  | 		verify.end(); | 
					
						
							| 
									
										
										
										
											2024-10-12 22:49:24 -04:00
										 |  |  | 		ActivityPub.helpers.log('[activitypub/verify] Attempting signed string verification'); | 
					
						
							| 
									
										
										
										
											2023-12-22 12:58:46 -05:00
										 |  |  | 		const verified = verify.verify(publicKeyPem, signature, 'base64'); | 
					
						
							| 
									
										
										
										
											2023-06-19 17:29:22 -04:00
										 |  |  | 		return verified; | 
					
						
							|  |  |  | 	} catch (e) { | 
					
						
							| 
									
										
										
										
											2024-10-12 22:49:24 -04:00
										 |  |  | 		ActivityPub.helpers.log('[activitypub/verify]   Failed, key retrieval or verification failure.'); | 
					
						
							| 
									
										
										
										
											2023-06-19 17:29:22 -04:00
										 |  |  | 		return false; | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-09 14:45:04 -04:00
										 |  |  | ActivityPub.get = async (type, id, uri, options) => { | 
					
						
							| 
									
										
										
										
											2025-01-20 12:05:17 -05:00
										 |  |  | 	if (!meta.config.activitypubEnabled) { | 
					
						
							|  |  |  | 		throw new Error('[[error:activitypub.not-enabled]]'); | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-09 14:45:04 -04:00
										 |  |  | 	options = { | 
					
						
							|  |  |  | 		cache: true, | 
					
						
							|  |  |  | 		...options, | 
					
						
							|  |  |  | 	}; | 
					
						
							| 
									
										
										
										
											2024-02-05 16:57:17 -05:00
										 |  |  | 	const cacheKey = [id, uri].join(';'); | 
					
						
							| 
									
										
										
										
											2024-06-04 12:31:13 -04:00
										 |  |  | 	const cached = requestCache.get(cacheKey); | 
					
						
							| 
									
										
										
										
											2024-07-09 14:45:04 -04:00
										 |  |  | 	if (options.cache && cached !== undefined) { | 
					
						
							| 
									
										
										
										
											2024-06-04 12:31:13 -04:00
										 |  |  | 		return cached; | 
					
						
							| 
									
										
										
										
											2024-01-12 11:27:55 -05:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-05 16:57:17 -05:00
										 |  |  | 	const keyData = await ActivityPub.getPrivateKey(type, id); | 
					
						
							|  |  |  | 	const headers = id >= 0 ? await ActivityPub.sign(keyData, uri) : {}; | 
					
						
							| 
									
										
										
										
											2024-10-12 22:49:24 -04:00
										 |  |  | 	ActivityPub.helpers.log(`[activitypub/get] ${uri}`); | 
					
						
							| 
									
										
										
										
											2024-02-26 11:39:32 -05:00
										 |  |  | 	try { | 
					
						
							|  |  |  | 		const { response, body } = await request.get(uri, { | 
					
						
							|  |  |  | 			headers: { | 
					
						
							|  |  |  | 				...headers, | 
					
						
							| 
									
										
										
										
											2024-07-16 11:36:39 -04:00
										 |  |  | 				...options.headers, | 
					
						
							| 
									
										
										
										
											2024-02-26 11:39:32 -05:00
										 |  |  | 				Accept: 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', | 
					
						
							|  |  |  | 			}, | 
					
						
							| 
									
										
										
										
											2024-04-04 12:22:13 -04:00
										 |  |  | 			timeout: 5000, | 
					
						
							| 
									
										
										
										
											2024-02-26 11:39:32 -05:00
										 |  |  | 		}); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		if (!String(response.statusCode).startsWith('2')) { | 
					
						
							| 
									
										
										
										
											2024-06-13 14:53:47 -04:00
										 |  |  | 			winston.verbose(`[activitypub/get] Received ${response.statusCode} when querying ${uri}`); | 
					
						
							| 
									
										
										
										
											2024-02-26 11:39:32 -05:00
										 |  |  | 			if (body.hasOwnProperty('error')) { | 
					
						
							| 
									
										
										
										
											2024-06-13 14:53:47 -04:00
										 |  |  | 				winston.verbose(`[activitypub/get] Error received: ${body.error}`); | 
					
						
							| 
									
										
										
										
											2024-02-26 11:39:32 -05:00
										 |  |  | 			} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-06-13 14:53:47 -04:00
										 |  |  | 			const e = new Error(`[[error:activitypub.get-failed]]`); | 
					
						
							|  |  |  | 			e.code = `ap_get_${response.statusCode}`; | 
					
						
							|  |  |  | 			throw e; | 
					
						
							| 
									
										
										
										
											2024-01-05 22:45:33 -05:00
										 |  |  | 		} | 
					
						
							| 
									
										
										
										
											2024-01-08 14:30:09 -05:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-26 11:39:32 -05:00
										 |  |  | 		requestCache.set(cacheKey, body); | 
					
						
							|  |  |  | 		return body; | 
					
						
							|  |  |  | 	} catch (e) { | 
					
						
							| 
									
										
										
										
											2024-06-13 14:53:47 -04:00
										 |  |  | 		if (String(e.code).startsWith('ap_get_')) { | 
					
						
							|  |  |  | 			throw e; | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-26 11:39:32 -05:00
										 |  |  | 		// Handle things like non-json body, etc.
 | 
					
						
							| 
									
										
										
										
											2024-06-13 14:53:47 -04:00
										 |  |  | 		const { cause } = e; | 
					
						
							|  |  |  | 		throw new Error(`[[error:activitypub.get-failed]]`, { cause }); | 
					
						
							| 
									
										
										
										
											2024-01-05 22:45:33 -05:00
										 |  |  | 	} | 
					
						
							| 
									
										
										
										
											2024-01-05 11:39:17 -05:00
										 |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-05-06 22:49:31 +02:00
										 |  |  | ActivityPub.retryQueue = lru({ name: 'activitypub-retry-queue', max: 4000, ttl: 1000 * 60 * 60 * 24 * 60 }); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-05-06 23:16:58 +02:00
										 |  |  | // handle clearing retry queue from another member of the cluster
 | 
					
						
							|  |  |  | pubsub.on(`activitypub-retry-queue:lruCache:del`, (keys) => { | 
					
						
							|  |  |  | 	if (Array.isArray(keys)) { | 
					
						
							|  |  |  | 		keys.forEach(key => clearTimeout(ActivityPub.retryQueue.get(key))); | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | }); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-05-06 22:49:31 +02:00
										 |  |  | async function sendMessage(uri, id, type, payload, attempts = 1) { | 
					
						
							|  |  |  | 	const keyData = await ActivityPub.getPrivateKey(type, id); | 
					
						
							|  |  |  | 	const headers = await ActivityPub.sign(keyData, uri, payload); | 
					
						
							| 
									
										
										
										
											2025-02-28 13:56:33 -05:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-05-06 22:49:31 +02:00
										 |  |  | 	try { | 
					
						
							|  |  |  | 		const { response, body } = await request.post(uri, { | 
					
						
							|  |  |  | 			headers: { | 
					
						
							|  |  |  | 				...headers, | 
					
						
							|  |  |  | 				'content-type': 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', | 
					
						
							|  |  |  | 			}, | 
					
						
							|  |  |  | 			body: payload, | 
					
						
							| 
									
										
										
										
											2024-12-16 22:38:10 -05:00
										 |  |  | 			timeout: 10000, // configurable?
 | 
					
						
							| 
									
										
										
										
											2024-05-06 22:49:31 +02:00
										 |  |  | 		}); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		if (String(response.statusCode).startsWith('2')) { | 
					
						
							| 
									
										
										
										
											2024-10-12 22:49:24 -04:00
										 |  |  | 			ActivityPub.helpers.log(`[activitypub/send] Successfully sent ${payload.type} to ${uri}`); | 
					
						
							| 
									
										
										
										
											2024-05-06 22:49:31 +02:00
										 |  |  | 		} else { | 
					
						
							| 
									
										
										
										
											2025-01-09 11:17:53 -05:00
										 |  |  | 			if (typeof body === 'object') { | 
					
						
							|  |  |  | 				throw new Error(JSON.stringify(body)); | 
					
						
							|  |  |  | 			} | 
					
						
							| 
									
										
										
										
											2024-05-06 22:49:31 +02:00
										 |  |  | 			throw new Error(String(body)); | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} catch (e) { | 
					
						
							| 
									
										
										
										
											2024-12-25 09:00:12 -05:00
										 |  |  | 		ActivityPub.helpers.log(`[activitypub/send] Could not send ${payload.type} to ${uri}; error: ${e.message}`); | 
					
						
							| 
									
										
										
										
											2024-05-06 22:49:31 +02:00
										 |  |  | 		// add to retry queue
 | 
					
						
							|  |  |  | 		if (attempts < 12) { // stop attempting after ~2 months
 | 
					
						
							|  |  |  | 			const timeout = (4 ** attempts) * 1000; // exponential backoff
 | 
					
						
							|  |  |  | 			const queueId = `${payload.type}:${payload.id}:${new URL(uri).hostname}`; | 
					
						
							|  |  |  | 			const timeoutId = setTimeout(() => sendMessage(uri, id, type, payload, attempts + 1), timeout); | 
					
						
							|  |  |  | 			ActivityPub.retryQueue.set(queueId, timeoutId); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-10-12 22:49:24 -04:00
										 |  |  | 			ActivityPub.helpers.log(`[activitypub/send] Added ${payload.type} to ${uri} to retry queue for ${timeout}ms`); | 
					
						
							| 
									
										
										
										
											2024-05-06 23:06:06 +02:00
										 |  |  | 		} else { | 
					
						
							|  |  |  | 			winston.warn(`[activitypub/send] Max attempts reached for ${payload.type} to ${uri}; giving up on sending`); | 
					
						
							| 
									
										
										
										
											2024-05-06 22:49:31 +02:00
										 |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-05 16:57:17 -05:00
										 |  |  | ActivityPub.send = async (type, id, targets, payload) => { | 
					
						
							| 
									
										
										
										
											2025-01-20 12:05:17 -05:00
										 |  |  | 	if (!meta.config.activitypubEnabled) { | 
					
						
							|  |  |  | 		return ActivityPub.helpers.log('[activitypub/send] Federation not enabled; not sending.'); | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-02 22:09:12 -05:00
										 |  |  | 	ActivityPub.helpers.log(`[activitypub/send] ${payload.id}`); | 
					
						
							| 
									
										
										
										
											2025-04-04 10:45:05 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-02 22:09:12 -05:00
										 |  |  | 	if (process.env.hasOwnProperty('CI')) { | 
					
						
							|  |  |  | 		ActivityPub._sent.set(payload.id, payload); | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-06-23 14:59:47 -04:00
										 |  |  | 	if (!Array.isArray(targets)) { | 
					
						
							|  |  |  | 		targets = [targets]; | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-01-26 21:39:20 -05:00
										 |  |  | 	const inboxes = await ActivityPub.resolveInboxes(targets); | 
					
						
							| 
									
										
										
										
											2023-06-23 14:59:47 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-09 23:58:25 +02:00
										 |  |  | 	const actor = ActivityPub.helpers.resolveActor(type, id); | 
					
						
							| 
									
										
										
										
											2024-02-06 10:40:46 -05:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-06-23 14:59:47 -04:00
										 |  |  | 	payload = { | 
					
						
							| 
									
										
										
										
											2023-12-13 13:15:03 -05:00
										 |  |  | 		'@context': 'https://www.w3.org/ns/activitystreams', | 
					
						
							| 
									
										
										
										
											2024-02-06 10:40:46 -05:00
										 |  |  | 		actor, | 
					
						
							| 
									
										
										
										
											2023-06-23 14:59:47 -04:00
										 |  |  | 		...payload, | 
					
						
							|  |  |  | 	}; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-28 12:59:06 -04:00
										 |  |  | 	// Runs in background... potentially a better queue is required... later.
 | 
					
						
							|  |  |  | 	batch.processArray( | 
					
						
							| 
									
										
										
										
											2024-05-06 22:52:48 +02:00
										 |  |  | 		inboxes, | 
					
						
							| 
									
										
										
										
											2024-05-06 23:06:06 +02:00
										 |  |  | 		async inboxBatch => Promise.all(inboxBatch.map(async uri => sendMessage(uri, id, type, payload))), | 
					
						
							|  |  |  | 		{ | 
					
						
							|  |  |  | 			batch: 50, | 
					
						
							|  |  |  | 			interval: 100, | 
					
						
							|  |  |  | 		}, | 
					
						
							| 
									
										
										
										
											2024-05-06 22:52:48 +02:00
										 |  |  | 	); | 
					
						
							| 
									
										
										
										
											2023-06-23 14:59:47 -04:00
										 |  |  | }; | 
					
						
							| 
									
										
										
										
											2024-05-14 12:06:59 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  | ActivityPub.record = async ({ id, type, actor }) => { | 
					
						
							|  |  |  | 	const now = Date.now(); | 
					
						
							|  |  |  | 	const { hostname } = new URL(actor); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	await Promise.all([ | 
					
						
							|  |  |  | 		db.sortedSetAdd(`activities:datetime`, now, id), | 
					
						
							|  |  |  | 		db.sortedSetAdd('domains:lastSeen', now, hostname), | 
					
						
							|  |  |  | 		analytics.increment(['activities', `activities:byType:${type}`, `activities:byHost:${hostname}`]), | 
					
						
							|  |  |  | 	]); | 
					
						
							|  |  |  | }; | 
					
						
							| 
									
										
										
										
											2024-10-23 11:28:55 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  | ActivityPub.buildRecipients = async function (object, { pid, uid, cid }) { | 
					
						
							|  |  |  | 	/** | 
					
						
							|  |  |  | 	 * - Builds a list of targets for activitypub.send to consume | 
					
						
							|  |  |  | 	 * - Extends to and cc since the activity can be addressed more widely | 
					
						
							|  |  |  | 	 * - Optional parameters: | 
					
						
							|  |  |  | 	 *     - `cid`: includes followers of the passed-in cid (local only) | 
					
						
							|  |  |  | 	 *     - `uid`: includes followers of the passed-in uid (local only) | 
					
						
							| 
									
										
										
										
											2024-10-24 13:17:32 -04:00
										 |  |  | 	 *     - `pid`: includes post announcers and all topic participants | 
					
						
							| 
									
										
										
										
											2024-10-23 11:28:55 -04:00
										 |  |  | 	 */ | 
					
						
							|  |  |  | 	let { to, cc } = object; | 
					
						
							|  |  |  | 	to = new Set(to); | 
					
						
							|  |  |  | 	cc = new Set(cc); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	let followers = []; | 
					
						
							|  |  |  | 	if (uid) { | 
					
						
							|  |  |  | 		followers = await db.getSortedSetMembers(`followersRemote:${uid}`); | 
					
						
							|  |  |  | 		const followersUrl = `${nconf.get('url')}/uid/${uid}/followers`; | 
					
						
							|  |  |  | 		if (!to.has(followersUrl)) { | 
					
						
							|  |  |  | 			cc.add(followersUrl); | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	if (cid) { | 
					
						
							|  |  |  | 		const cidFollowers = await ActivityPub.notes.getCategoryFollowers(cid); | 
					
						
							|  |  |  | 		followers = followers.concat(cidFollowers); | 
					
						
							|  |  |  | 		const followersUrl = `${nconf.get('url')}/category/${cid}/followers`; | 
					
						
							|  |  |  | 		if (!to.has(followersUrl)) { | 
					
						
							|  |  |  | 			cc.add(followersUrl); | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	const targets = new Set([...followers, ...to, ...cc]); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Remove any ids that aren't asserted actors
 | 
					
						
							|  |  |  | 	const exists = await db.isSortedSetMembers('usersRemote:lastCrawled', [...targets]); | 
					
						
							|  |  |  | 	Array.from(targets).forEach((uri, idx) => { | 
					
						
							|  |  |  | 		if (!exists[idx]) { | 
					
						
							|  |  |  | 			targets.delete(uri); | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	}); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Topic posters, post announcers and their followers
 | 
					
						
							|  |  |  | 	if (pid) { | 
					
						
							|  |  |  | 		const tid = await posts.getPostField(pid, 'tid'); | 
					
						
							|  |  |  | 		const participants = (await db.getSortedSetMembers(`tid:${tid}:posters`)) | 
					
						
							|  |  |  | 			.filter(uid => !utils.isNumber(uid)); // remote users only
 | 
					
						
							|  |  |  | 		const announcers = (await ActivityPub.notes.announce.list({ pid })).map(({ actor }) => actor); | 
					
						
							|  |  |  | 		const auxiliaries = Array.from(new Set([...participants, ...announcers])); | 
					
						
							|  |  |  | 		const auxiliaryFollowers = (await user.getUsersFields(auxiliaries, ['followersUrl'])) | 
					
						
							|  |  |  | 			.filter(o => o.hasOwnProperty('followersUrl')) | 
					
						
							|  |  |  | 			.map(({ followersUrl }) => followersUrl); | 
					
						
							|  |  |  | 		[...auxiliaries].forEach(uri => uri && targets.add(uri)); | 
					
						
							|  |  |  | 		[...auxiliaries, ...auxiliaryFollowers].forEach(uri => uri && cc.add(uri)); | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return { | 
					
						
							|  |  |  | 		to: [...to], | 
					
						
							|  |  |  | 		cc: [...cc], | 
					
						
							|  |  |  | 		targets, | 
					
						
							|  |  |  | 	}; | 
					
						
							|  |  |  | }; | 
					
						
							| 
									
										
										
										
											2024-10-23 15:25:16 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  | ActivityPub.probe = async ({ uid, url }) => { | 
					
						
							|  |  |  | 	/** | 
					
						
							|  |  |  | 	 * Checks whether a passed-in id or URL is an ActivityPub object and can be mapped to a local representation | 
					
						
							|  |  |  | 	 *   - `uid` is optional (links to private messages won't match without uid) | 
					
						
							|  |  |  | 	 *   - Returns a relative path if already available, true if not, and false otherwise. | 
					
						
							|  |  |  | 	 */ | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Known resources
 | 
					
						
							| 
									
										
										
										
											2024-12-30 14:53:56 -05:00
										 |  |  | 	const [isNote, isMessage, isActor, isActorUrl] = await Promise.all([ | 
					
						
							| 
									
										
										
										
											2024-10-23 15:25:16 -04:00
										 |  |  | 		posts.exists(url), | 
					
						
							|  |  |  | 		messaging.messageExists(url), | 
					
						
							| 
									
										
										
										
											2024-12-30 14:53:56 -05:00
										 |  |  | 		db.isSortedSetMember('usersRemote:lastCrawled', url), // if url is same as id
 | 
					
						
							| 
									
										
										
										
											2024-10-23 15:25:16 -04:00
										 |  |  | 		db.isObjectField('remoteUrl:uid', url), | 
					
						
							|  |  |  | 	]); | 
					
						
							|  |  |  | 	switch (true) { | 
					
						
							|  |  |  | 		case isNote: { | 
					
						
							|  |  |  | 			return `/post/${encodeURIComponent(url)}`; | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		case isMessage: { | 
					
						
							|  |  |  | 			if (uid) { | 
					
						
							|  |  |  | 				const { roomId } = await messaging.getMessageFields(url, ['roomId']); | 
					
						
							|  |  |  | 				const canView = await messaging.canViewMessage(url, roomId, uid); | 
					
						
							|  |  |  | 				if (canView) { | 
					
						
							|  |  |  | 					return `/message/${encodeURIComponent(url)}`; | 
					
						
							|  |  |  | 				} | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 			break; | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		case isActor: { | 
					
						
							| 
									
										
										
										
											2024-12-30 14:53:56 -05:00
										 |  |  | 			const slug = await user.getUserField(url, 'userslug'); | 
					
						
							|  |  |  | 			return `/user/${slug}`; | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		case isActorUrl: { | 
					
						
							| 
									
										
										
										
											2024-10-23 15:25:16 -04:00
										 |  |  | 			const uid = await db.getObjectField('remoteUrl:uid', url); | 
					
						
							|  |  |  | 			const slug = await user.getUserField(uid, 'userslug'); | 
					
						
							|  |  |  | 			return `/user/${slug}`; | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Cached result
 | 
					
						
							|  |  |  | 	if (probeCache.has(url)) { | 
					
						
							|  |  |  | 		return probeCache.get(url); | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Opportunistic HEAD
 | 
					
						
							| 
									
										
										
										
											2024-10-30 10:59:05 -04:00
										 |  |  | 	async function checkHeader(timeout) { | 
					
						
							|  |  |  | 		const { response } = await request.head(url, { | 
					
						
							|  |  |  | 			timeout, | 
					
						
							|  |  |  | 		}); | 
					
						
							| 
									
										
										
										
											2024-10-23 15:25:16 -04:00
										 |  |  | 		const { headers } = response; | 
					
						
							|  |  |  | 		if (headers && headers.link) { | 
					
						
							|  |  |  | 			let parts = headers.link.split(';'); | 
					
						
							|  |  |  | 			parts.shift(); | 
					
						
							|  |  |  | 			parts = parts | 
					
						
							|  |  |  | 				.map(p => p.trim()) | 
					
						
							|  |  |  | 				.reduce((memo, cur) => { | 
					
						
							|  |  |  | 					cur = cur.split('='); | 
					
						
							|  |  |  | 					memo[cur[0]] = cur[1].slice(1, -1); | 
					
						
							|  |  |  | 					return memo; | 
					
						
							|  |  |  | 				}, {}); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			if (parts.rel === 'alternate' && parts.type === 'application/activity+json') { | 
					
						
							|  |  |  | 				probeCache.set(url, true); | 
					
						
							|  |  |  | 				return true; | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2024-10-30 10:59:05 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  | 		return false; | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	try { | 
					
						
							| 
									
										
										
										
											2024-10-31 11:59:42 -04:00
										 |  |  | 		return await checkHeader(meta.config.activitypubProbeTimeout || 2000); | 
					
						
							| 
									
										
										
										
											2024-10-23 15:25:16 -04:00
										 |  |  | 	} catch (e) { | 
					
						
							| 
									
										
										
										
											2024-10-30 10:59:05 -04:00
										 |  |  | 		if (e.name === 'TimeoutError') { | 
					
						
							|  |  |  | 			// Return early but retry for caching purposes
 | 
					
						
							|  |  |  | 			checkHeader(1000 * 60).then((result) => { | 
					
						
							|  |  |  | 				probeCache.set(url, result); | 
					
						
							| 
									
										
										
										
											2024-12-25 10:43:02 -05:00
										 |  |  | 			}).catch(err => ActivityPub.helpers.log(err.stack)); | 
					
						
							| 
									
										
										
										
											2024-10-30 10:59:05 -04:00
										 |  |  | 			return false; | 
					
						
							|  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2024-10-23 15:25:16 -04:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	probeCache.set(url, false); | 
					
						
							|  |  |  | 	return false; | 
					
						
							|  |  |  | }; |