| 
									
										
										
										
											2023-05-29 17:42:44 -04:00
										 |  |  | 'use strict'; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-06-23 14:59:47 -04:00
										 |  |  | const { generateKeyPairSync } = require('crypto'); | 
					
						
							| 
									
										
										
										
											2023-06-19 17:29:22 -04:00
										 |  |  | const winston = require('winston'); | 
					
						
							| 
									
										
										
										
											2023-06-28 14:59:39 -04:00
										 |  |  | const nconf = require('nconf'); | 
					
						
							| 
									
										
										
										
											2023-12-11 14:35:04 -05:00
										 |  |  | const validator = require('validator'); | 
					
						
							| 
									
										
										
										
											2023-05-29 17:42:44 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-09 23:58:00 +02:00
										 |  |  | const posts = require('../posts'); | 
					
						
							| 
									
										
										
										
											2024-04-10 18:50:41 +02:00
										 |  |  | const categories = require('../categories'); | 
					
						
							| 
									
										
										
										
											2023-12-19 14:33:38 -05:00
										 |  |  | const request = require('../request'); | 
					
						
							| 
									
										
										
										
											2023-06-19 17:29:22 -04:00
										 |  |  | const db = require('../database'); | 
					
						
							| 
									
										
										
										
											2023-06-16 11:26:25 -04:00
										 |  |  | const ttl = require('../cache/ttl'); | 
					
						
							| 
									
										
										
										
											2023-06-28 14:59:39 -04:00
										 |  |  | const user = require('../user'); | 
					
						
							| 
									
										
										
										
											2024-04-08 20:06:26 +02:00
										 |  |  | const activitypub = require('.'); | 
					
						
							| 
									
										
										
										
											2023-06-16 11:26:25 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  | const webfingerCache = ttl({ ttl: 1000 * 60 * 60 * 24 }); // 24 hours
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-05-29 17:42:44 -04:00
										 |  |  | const Helpers = module.exports; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-01-05 11:38:26 -05:00
										 |  |  | Helpers.isUri = (value) => { | 
					
						
							|  |  |  | 	if (typeof value !== 'string') { | 
					
						
							|  |  |  | 		value = String(value); | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	const protocols = ['https']; | 
					
						
							|  |  |  | 	if (process.env.CI === 'true') { | 
					
						
							|  |  |  | 		protocols.push('http'); | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return validator.isURL(value, { | 
					
						
							|  |  |  | 		require_protocol: true, | 
					
						
							|  |  |  | 		require_host: true, | 
					
						
							|  |  |  | 		protocols, | 
					
						
							|  |  |  | 		require_valid_protocol: true, | 
					
						
							|  |  |  | 		require_tld: false, // temporary — for localhost
 | 
					
						
							|  |  |  | 	}); | 
					
						
							|  |  |  | }; | 
					
						
							| 
									
										
										
										
											2024-01-04 16:23:09 -05:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-05-29 17:42:44 -04:00
										 |  |  | Helpers.query = async (id) => { | 
					
						
							|  |  |  | 	const [username, hostname] = id.split('@'); | 
					
						
							|  |  |  | 	if (!username || !hostname) { | 
					
						
							|  |  |  | 		return false; | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-06-16 11:26:25 -04:00
										 |  |  | 	if (webfingerCache.has(id)) { | 
					
						
							|  |  |  | 		return webfingerCache.get(id); | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-05-29 17:42:44 -04:00
										 |  |  | 	// Make a webfinger query to retrieve routing information
 | 
					
						
							| 
									
										
										
										
											2024-03-11 14:41:05 -04:00
										 |  |  | 	let response; | 
					
						
							|  |  |  | 	let body; | 
					
						
							|  |  |  | 	try { | 
					
						
							|  |  |  | 		({ response, body } = await request.get(`https://${hostname}/.well-known/webfinger?resource=acct:${id}`)); | 
					
						
							|  |  |  | 	} catch (e) { | 
					
						
							|  |  |  | 		return false; | 
					
						
							|  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2023-05-29 17:42:44 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-12-19 14:33:38 -05:00
										 |  |  | 	if (response.statusCode !== 200 || !body.hasOwnProperty('links')) { | 
					
						
							| 
									
										
										
										
											2023-05-29 17:42:44 -04:00
										 |  |  | 		return false; | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Parse links to find actor endpoint
 | 
					
						
							| 
									
										
										
										
											2024-04-08 20:06:26 +02:00
										 |  |  | 	let actorUri = body.links.filter(link => activitypub._constants.acceptableTypes.includes(link.type) && link.rel === 'self'); | 
					
						
							| 
									
										
										
										
											2023-05-29 17:42:44 -04:00
										 |  |  | 	if (actorUri.length) { | 
					
						
							|  |  |  | 		actorUri = actorUri.pop(); | 
					
						
							|  |  |  | 		({ href: actorUri } = actorUri); | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-03-05 09:56:15 -05:00
										 |  |  | 	const { subject, publicKey } = body; | 
					
						
							|  |  |  | 	const payload = { subject, username, hostname, actorUri, publicKey }; | 
					
						
							| 
									
										
										
										
											2023-06-19 17:29:22 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-03-05 09:56:15 -05:00
										 |  |  | 	const claimedId = subject.slice(5); | 
					
						
							|  |  |  | 	webfingerCache.set(claimedId, payload); | 
					
						
							|  |  |  | 	if (claimedId !== id) { | 
					
						
							|  |  |  | 		webfingerCache.set(id, payload); | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return payload; | 
					
						
							| 
									
										
										
										
											2023-06-19 17:29:22 -04:00
										 |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-05 16:57:17 -05:00
										 |  |  | Helpers.generateKeys = async (type, id) => { | 
					
						
							|  |  |  | 	winston.verbose(`[activitypub] Generating RSA key-pair for ${type} ${id}`); | 
					
						
							| 
									
										
										
										
											2023-06-19 17:29:22 -04:00
										 |  |  | 	const { | 
					
						
							|  |  |  | 		publicKey, | 
					
						
							|  |  |  | 		privateKey, | 
					
						
							|  |  |  | 	} = generateKeyPairSync('rsa', { | 
					
						
							|  |  |  | 		modulusLength: 2048, | 
					
						
							|  |  |  | 		publicKeyEncoding: { | 
					
						
							|  |  |  | 			type: 'spki', | 
					
						
							|  |  |  | 			format: 'pem', | 
					
						
							|  |  |  | 		}, | 
					
						
							|  |  |  | 		privateKeyEncoding: { | 
					
						
							|  |  |  | 			type: 'pkcs8', | 
					
						
							|  |  |  | 			format: 'pem', | 
					
						
							|  |  |  | 		}, | 
					
						
							|  |  |  | 	}); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-05 16:57:17 -05:00
										 |  |  | 	await db.setObject(`${type}:${id}:keys`, { publicKey, privateKey }); | 
					
						
							| 
									
										
										
										
											2023-06-19 17:29:22 -04:00
										 |  |  | 	return { publicKey, privateKey }; | 
					
						
							| 
									
										
										
										
											2023-05-29 17:42:44 -04:00
										 |  |  | }; | 
					
						
							| 
									
										
										
										
											2023-06-28 14:59:39 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-05 16:57:17 -05:00
										 |  |  | Helpers.resolveLocalId = async (input) => { | 
					
						
							| 
									
										
										
										
											2024-01-04 16:23:09 -05:00
										 |  |  | 	if (Helpers.isUri(input)) { | 
					
						
							| 
									
										
										
										
											2024-04-09 23:58:00 +02:00
										 |  |  | 		const { host, pathname, hash } = new URL(input); | 
					
						
							| 
									
										
										
										
											2023-12-11 14:35:04 -05:00
										 |  |  | 
 | 
					
						
							|  |  |  | 		if (host === nconf.get('url_parsed').host) { | 
					
						
							| 
									
										
										
										
											2024-02-05 16:57:17 -05:00
										 |  |  | 			const [prefix, value] = pathname.replace(nconf.get('relative_path'), '').split('/').filter(Boolean); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-09 23:58:00 +02:00
										 |  |  | 			let activityData = {}; | 
					
						
							|  |  |  | 			if (hash.startsWith('#activity')) { | 
					
						
							|  |  |  | 				const [, activity, data] = hash.split('/', 3); | 
					
						
							|  |  |  | 				activityData = { activity, data }; | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-05 16:57:17 -05:00
										 |  |  | 			switch (prefix) { | 
					
						
							|  |  |  | 				case 'uid': | 
					
						
							| 
									
										
										
										
											2024-04-09 23:58:00 +02:00
										 |  |  | 					return { type: 'user', id: value, ...activityData }; | 
					
						
							| 
									
										
										
										
											2024-02-05 16:57:17 -05:00
										 |  |  | 
 | 
					
						
							|  |  |  | 				case 'post': | 
					
						
							| 
									
										
										
										
											2024-04-09 23:58:00 +02:00
										 |  |  | 					return { type: 'post', id: value, ...activityData }; | 
					
						
							| 
									
										
										
										
											2024-02-05 16:57:17 -05:00
										 |  |  | 
 | 
					
						
							|  |  |  | 				case 'category': | 
					
						
							| 
									
										
										
										
											2024-04-09 23:58:00 +02:00
										 |  |  | 					return { type: 'category', id: value, ...activityData }; | 
					
						
							| 
									
										
										
										
											2024-02-05 16:57:17 -05:00
										 |  |  | 
 | 
					
						
							|  |  |  | 				case 'user': { | 
					
						
							|  |  |  | 					const uid = await user.getUidByUserslug(value); | 
					
						
							| 
									
										
										
										
											2024-04-09 23:58:00 +02:00
										 |  |  | 					return { type: 'user', id: uid, ...activityData }; | 
					
						
							| 
									
										
										
										
											2024-02-05 16:57:17 -05:00
										 |  |  | 				} | 
					
						
							| 
									
										
										
										
											2024-01-29 16:59:13 -05:00
										 |  |  | 			} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-09 23:58:00 +02:00
										 |  |  | 			return { type: null, id: null, ...activityData }; | 
					
						
							| 
									
										
										
										
											2023-12-11 14:35:04 -05:00
										 |  |  | 		} | 
					
						
							| 
									
										
										
										
											2024-02-09 11:15:03 -05:00
										 |  |  | 
 | 
					
						
							|  |  |  | 		return { type: null, id: null }; | 
					
						
							| 
									
										
										
										
											2024-02-28 12:54:54 -05:00
										 |  |  | 	} else if (String(input).indexOf('@') !== -1) { // Webfinger
 | 
					
						
							| 
									
										
										
										
											2024-02-05 16:57:17 -05:00
										 |  |  | 		const [slug] = input.replace(/^acct:/, '').split('@'); | 
					
						
							|  |  |  | 		const uid = await user.getUidByUserslug(slug); | 
					
						
							|  |  |  | 		return { type: 'user', id: uid }; | 
					
						
							| 
									
										
										
										
											2024-02-01 15:59:29 -05:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-09 11:15:03 -05:00
										 |  |  | 	return { type: null, id: null }; | 
					
						
							| 
									
										
										
										
											2024-02-01 15:59:29 -05:00
										 |  |  | }; | 
					
						
							| 
									
										
										
										
											2024-04-09 23:58:00 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | Helpers.resolveActor = (type, id) => { | 
					
						
							|  |  |  | 	switch (type) { | 
					
						
							|  |  |  | 		case 'user': | 
					
						
							|  |  |  | 		case 'uid': { | 
					
						
							|  |  |  | 			return `${nconf.get('url')}${id > 0 ? `/uid/${id}` : '/actor'}`; | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		case 'category': | 
					
						
							|  |  |  | 		case 'cid': { | 
					
						
							|  |  |  | 			return `${nconf.get('url')}/category/${id}`; | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		default: | 
					
						
							|  |  |  | 			throw new Error('[[error:activitypub.invalid-id]]'); | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | Helpers.resolveActivity = async (activity, data, id, resolved) => { | 
					
						
							|  |  |  | 	switch (activity.toLowerCase()) { | 
					
						
							|  |  |  | 		case 'follow': { | 
					
						
							|  |  |  | 			const actor = await Helpers.resolveActor(resolved.type, resolved.id); | 
					
						
							|  |  |  | 			const { actorUri: targetUri } = await Helpers.query(data); | 
					
						
							|  |  |  | 			return { | 
					
						
							|  |  |  | 				'@context': 'https://www.w3.org/ns/activitystreams', | 
					
						
							|  |  |  | 				actor, | 
					
						
							|  |  |  | 				id, | 
					
						
							|  |  |  | 				type: 'Follow', | 
					
						
							|  |  |  | 				object: targetUri, | 
					
						
							|  |  |  | 			}; | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		default: { | 
					
						
							|  |  |  | 			throw new Error('[[error:activitypub.not-implemented]]'); | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | Helpers.resolveObjects = async (ids) => { | 
					
						
							|  |  |  | 	if (!Array.isArray(ids)) { | 
					
						
							|  |  |  | 		ids = [ids]; | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	const objects = await Promise.all(ids.map(async (id) => { | 
					
						
							|  |  |  | 		const { type, id: resolvedId, activity, data: activityData } = await Helpers.resolveLocalId(id); | 
					
						
							|  |  |  | 		if (activity) { | 
					
						
							|  |  |  | 			return Helpers.resolveActivity(activity, activityData, id, { type, id: resolvedId }); | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		switch (type) { | 
					
						
							|  |  |  | 			case 'user': { | 
					
						
							| 
									
										
										
										
											2024-04-10 18:50:41 +02:00
										 |  |  | 				if (!await user.exists(resolvedId)) { | 
					
						
							|  |  |  | 					throw new Error('[[error:activitypub.invalid-id]]'); | 
					
						
							|  |  |  | 				} | 
					
						
							| 
									
										
										
										
											2024-04-09 23:58:00 +02:00
										 |  |  | 				return activitypub.mocks.actors.user(resolvedId); | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 			case 'post': { | 
					
						
							|  |  |  | 				const post = (await posts.getPostSummaryByPids( | 
					
						
							|  |  |  | 					[resolvedId], | 
					
						
							|  |  |  | 					activitypub._constants.uid, | 
					
						
							|  |  |  | 					{ stripTags: false } | 
					
						
							|  |  |  | 				)).pop(); | 
					
						
							|  |  |  | 				if (!post) { | 
					
						
							| 
									
										
										
										
											2024-04-10 18:50:41 +02:00
										 |  |  | 					throw new Error('[[error:activitypub.invalid-id]]'); | 
					
						
							| 
									
										
										
										
											2024-04-09 23:58:00 +02:00
										 |  |  | 				} | 
					
						
							|  |  |  | 				return activitypub.mocks.note(post); | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 			case 'category': { | 
					
						
							| 
									
										
										
										
											2024-04-10 18:50:41 +02:00
										 |  |  | 				if (!await categories.exists(resolvedId)) { | 
					
						
							|  |  |  | 					throw new Error('[[error:activitypub.invalid-id]]'); | 
					
						
							|  |  |  | 				} | 
					
						
							|  |  |  | 				return activitypub.mocks.actors.category(resolvedId); | 
					
						
							| 
									
										
										
										
											2024-04-09 23:58:00 +02:00
										 |  |  | 			} | 
					
						
							|  |  |  | 			default: { | 
					
						
							|  |  |  | 				return activitypub.get('uid', 0, id); | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	})); | 
					
						
							|  |  |  | 	return objects.length === 1 ? objects[0] : objects; | 
					
						
							|  |  |  | }; |