| 
									
										
										
										
											2023-05-29 17:42:44 -04:00
										 |  |  | 'use strict'; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-06-23 14:59:47 -04:00
										 |  |  | const { generateKeyPairSync } = require('crypto'); | 
					
						
							| 
									
										
										
										
											2023-06-28 14:59:39 -04:00
										 |  |  | const nconf = require('nconf'); | 
					
						
							| 
									
										
										
										
											2023-12-11 14:35:04 -05:00
										 |  |  | const validator = require('validator'); | 
					
						
							| 
									
										
										
										
											2024-04-29 16:16:07 -04:00
										 |  |  | const cheerio = require('cheerio'); | 
					
						
							| 
									
										
										
										
											2023-05-29 17:42:44 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-29 16:16:07 -04:00
										 |  |  | const meta = require('../meta'); | 
					
						
							| 
									
										
										
										
											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
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-06-07 12:13:28 -04:00
										 |  |  | const webfingerRegex = /^(@|acct:)?[\w-]+@.+$/; | 
					
						
							| 
									
										
										
										
											2024-06-04 12:31:13 -04:00
										 |  |  | const webfingerCache = ttl({ | 
					
						
							|  |  |  | 	max: 5000, | 
					
						
							|  |  |  | 	ttl: 1000 * 60 * 60 * 24, // 24 hours
 | 
					
						
							|  |  |  | }); | 
					
						
							| 
									
										
										
										
											2023-06-16 11:26:25 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											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); | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return validator.isURL(value, { | 
					
						
							|  |  |  | 		require_protocol: true, | 
					
						
							|  |  |  | 		require_host: true, | 
					
						
							| 
									
										
										
										
											2024-04-25 20:05:53 +02:00
										 |  |  | 		protocols: activitypub._constants.acceptedProtocols, | 
					
						
							| 
									
										
										
										
											2024-01-05 11:38:26 -05:00
										 |  |  | 		require_valid_protocol: true, | 
					
						
							|  |  |  | 		require_tld: false, // temporary — for localhost
 | 
					
						
							|  |  |  | 	}); | 
					
						
							|  |  |  | }; | 
					
						
							| 
									
										
										
										
											2024-01-04 16:23:09 -05:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-05-07 10:11:36 -04:00
										 |  |  | Helpers.isWebfinger = (value) => { | 
					
						
							|  |  |  | 	// N.B. returns normalized handle, so truthy check!
 | 
					
						
							|  |  |  | 	if (webfingerRegex.test(value) && !Helpers.isUri(value)) { | 
					
						
							|  |  |  | 		if (value.startsWith('@')) { | 
					
						
							|  |  |  | 			return value.slice(1); | 
					
						
							|  |  |  | 		} else if (value.startsWith('acct:')) { | 
					
						
							|  |  |  | 			return value.slice(5); | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		return value; | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return false; | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-05-29 17:42:44 -04:00
										 |  |  | Helpers.query = async (id) => { | 
					
						
							| 
									
										
										
										
											2024-04-25 12:59:05 +02:00
										 |  |  | 	const isUri = Helpers.isUri(id); | 
					
						
							| 
									
										
										
										
											2024-04-25 17:16:30 +02:00
										 |  |  | 	// username@host ids use acct: URI schema
 | 
					
						
							|  |  |  | 	const uri = isUri ? new URL(id) : new URL(`acct:${id}`); | 
					
						
							|  |  |  | 	// JS doesn't parse anything other than protocol and pathname from acct: URIs, so we need to just split id manually
 | 
					
						
							|  |  |  | 	const [username, hostname] = isUri ? [uri.pathname || uri.href, uri.host] : id.split('@'); | 
					
						
							|  |  |  | 	if (!username || !hostname) { | 
					
						
							| 
									
										
										
										
											2023-05-29 17:42:44 -04:00
										 |  |  | 		return false; | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-06-04 12:31:13 -04:00
										 |  |  | 	const cached = webfingerCache.get(id); | 
					
						
							|  |  |  | 	if (cached !== undefined) { | 
					
						
							|  |  |  | 		return cached; | 
					
						
							| 
									
										
										
										
											2023-06-16 11:26:25 -04:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-25 17:16:30 +02:00
										 |  |  | 	const query = new URLSearchParams({ resource: uri }); | 
					
						
							| 
									
										
										
										
											2024-04-25 12:59:05 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											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 { | 
					
						
							| 
									
										
										
										
											2024-04-25 13:16:05 +02:00
										 |  |  | 		({ response, body } = await request.get(`https://${hostname}/.well-known/webfinger?${query}`)); | 
					
						
							| 
									
										
										
										
											2024-03-11 14:41:05 -04:00
										 |  |  | 	} 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-04-25 17:16:30 +02:00
										 |  |  | 	const claimedId = new URL(subject).pathname; | 
					
						
							| 
									
										
										
										
											2024-03-05 09:56:15 -05:00
										 |  |  | 	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) => { | 
					
						
							| 
									
										
										
										
											2024-06-07 11:56:58 -04:00
										 |  |  | 	// 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')) { | 
					
						
							| 
									
										
										
										
											2024-06-11 22:55:45 +02:00
										 |  |  | 				const [, activity, data, timestamp] = hash.split('/', 4); | 
					
						
							|  |  |  | 				activityData = { activity, data, timestamp }; | 
					
						
							| 
									
										
										
										
											2024-04-09 23:58:00 +02:00
										 |  |  | 			} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											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
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-15 14:40:26 -04:00
										 |  |  | 				case 'cid': | 
					
						
							| 
									
										
										
										
											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-04-26 11:30:08 -04:00
										 |  |  | 		input = decodeURIComponent(input); | 
					
						
							| 
									
										
										
										
											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, | 
					
						
							|  |  |  | 			}; | 
					
						
							|  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2024-04-17 19:19:09 +02:00
										 |  |  | 		case 'announce': | 
					
						
							|  |  |  | 		case 'create': { | 
					
						
							|  |  |  | 			const object = await Helpers.resolveObjects(resolved.id); | 
					
						
							|  |  |  | 			// local create activities are assumed to come from the user who created the underlying object
 | 
					
						
							|  |  |  | 			const actor = object.attributedTo || object.actor; | 
					
						
							|  |  |  | 			return { | 
					
						
							|  |  |  | 				'@context': 'https://www.w3.org/ns/activitystreams', | 
					
						
							|  |  |  | 				actor, | 
					
						
							|  |  |  | 				id, | 
					
						
							|  |  |  | 				type: 'Create', | 
					
						
							|  |  |  | 				object, | 
					
						
							|  |  |  | 			}; | 
					
						
							|  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2024-04-09 23:58:00 +02:00
										 |  |  | 		default: { | 
					
						
							|  |  |  | 			throw new Error('[[error:activitypub.not-implemented]]'); | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-14 02:42:30 +02:00
										 |  |  | Helpers.mapToLocalType = (type) => { | 
					
						
							|  |  |  | 	if (type === 'Person') { | 
					
						
							|  |  |  | 		return 'user'; | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	if (type === 'Group') { | 
					
						
							|  |  |  | 		return 'category'; | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	if (type === 'Hashtag') { | 
					
						
							|  |  |  | 		return 'tag'; | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	if (activitypub._constants.acceptedPostTypes.includes(type)) { | 
					
						
							|  |  |  | 		return 'post'; | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | }; | 
					
						
							| 
									
										
										
										
											2024-04-09 23:58:00 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | Helpers.resolveObjects = async (ids) => { | 
					
						
							|  |  |  | 	if (!Array.isArray(ids)) { | 
					
						
							|  |  |  | 		ids = [ids]; | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	const objects = await Promise.all(ids.map(async (id) => { | 
					
						
							| 
									
										
										
										
											2024-04-17 19:19:09 +02:00
										 |  |  | 		// try to get a local ID first
 | 
					
						
							| 
									
										
										
										
											2024-04-09 23:58:00 +02:00
										 |  |  | 		const { type, id: resolvedId, activity, data: activityData } = await Helpers.resolveLocalId(id); | 
					
						
							| 
									
										
										
										
											2024-04-17 19:19:09 +02:00
										 |  |  | 		// activity data is only resolved for local IDs - so this will be false for remote posts
 | 
					
						
							| 
									
										
										
										
											2024-04-09 23:58:00 +02:00
										 |  |  | 		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
										 |  |  | 			} | 
					
						
							| 
									
										
										
										
											2024-04-17 19:19:09 +02:00
										 |  |  | 			// if the type is not recognized, assume it's not a local ID and fetch the object from its origin
 | 
					
						
							| 
									
										
										
										
											2024-04-09 23:58:00 +02:00
										 |  |  | 			default: { | 
					
						
							|  |  |  | 				return activitypub.get('uid', 0, id); | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	})); | 
					
						
							|  |  |  | 	return objects.length === 1 ? objects[0] : objects; | 
					
						
							|  |  |  | }; | 
					
						
							| 
									
										
										
										
											2024-04-29 16:16:07 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  | Helpers.generateTitle = (html) => { | 
					
						
							|  |  |  | 	// Given an html string, generates a more appropriate title if possible
 | 
					
						
							|  |  |  | 	const $ = cheerio.load(html); | 
					
						
							|  |  |  | 	let title; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Try the first paragraph element
 | 
					
						
							|  |  |  | 	title = $('h1, h2, h3, h4, h5, h6, title, p, span').first().text(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Fall back to newline splitting (i.e. if no paragraph elements)
 | 
					
						
							| 
									
										
										
										
											2024-04-30 11:41:34 -04:00
										 |  |  | 	title = title || html.split('\n').filter(Boolean).shift(); | 
					
						
							| 
									
										
										
										
											2024-04-29 16:16:07 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	// Split sentences and use only first one
 | 
					
						
							| 
									
										
										
										
											2024-04-30 11:41:34 -04:00
										 |  |  | 	const sentences = title | 
					
						
							|  |  |  | 		.split(/(\.|\?|!)\s/) | 
					
						
							|  |  |  | 		.reduce((memo, cur, idx, sentences) => { | 
					
						
							|  |  |  | 			if (idx % 2) { | 
					
						
							|  |  |  | 				memo.push(`${sentences[idx - 1]}${cur}`); | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			return memo; | 
					
						
							|  |  |  | 		}, []); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	if (sentences.length > 1) { | 
					
						
							|  |  |  | 		title = sentences.shift(); | 
					
						
							| 
									
										
										
										
											2024-04-29 16:16:07 -04:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Truncate down if too long
 | 
					
						
							|  |  |  | 	if (title.length > meta.config.maximumTitleLength) { | 
					
						
							|  |  |  | 		title = `${title.slice(0, meta.config.maximumTitleLength - 3)}...`; | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return title; | 
					
						
							|  |  |  | }; | 
					
						
							| 
									
										
										
										
											2024-06-12 20:31:36 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  | Helpers.remoteAnchorToLocalProfile = async (content) => { | 
					
						
							|  |  |  | 	const anchorRegex = /<a.*?href=['"](.+?)['"].*?>(.*?)<\/a>/ig; | 
					
						
							|  |  |  | 	const anchors = content.matchAll(anchorRegex); | 
					
						
							|  |  |  | 	const urls = new Set(); | 
					
						
							|  |  |  | 	const matches = []; | 
					
						
							|  |  |  | 	for (const anchor of anchors) { | 
					
						
							|  |  |  | 		const [match, url] = anchor; | 
					
						
							|  |  |  | 		matches.push([match, url]); | 
					
						
							|  |  |  | 		urls.add(url); | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	if (!urls.size) { | 
					
						
							|  |  |  | 		return content; | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Filter out urls that don't backreference to a remote id
 | 
					
						
							|  |  |  | 	const urlsArray = Array.from(urls); | 
					
						
							|  |  |  | 	const [backrefs, urlAsIdExists] = await Promise.all([ | 
					
						
							|  |  |  | 		db.getObjectFields('remoteUrl:uid', urlsArray), | 
					
						
							|  |  |  | 		db.isSortedSetMembers('usersRemote:lastCrawled', urlsArray), | 
					
						
							|  |  |  | 	]); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	const urlMap = new Map(); | 
					
						
							|  |  |  | 	urlsArray.forEach((url, index) => { | 
					
						
							|  |  |  | 		if (backrefs[url] || urlAsIdExists[index]) { | 
					
						
							|  |  |  | 			urlMap.set(url, backrefs[url] || url); | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	}); | 
					
						
							|  |  |  | 	let slugs = await user.getUsersFields(Array.from(urlMap.values()), ['userslug']); | 
					
						
							|  |  |  | 	slugs = slugs.map(({ userslug }) => userslug); | 
					
						
							|  |  |  | 	Array.from(urlMap.keys()).forEach((url, idx) => { | 
					
						
							|  |  |  | 		urlMap.set(url, `/user/${encodeURIComponent(slugs[idx])}`); | 
					
						
							|  |  |  | 	}); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Modify existing anchors to local profile
 | 
					
						
							|  |  |  | 	matches.forEach(([match, href]) => { | 
					
						
							|  |  |  | 		const replacementHref = urlMap.get(href); | 
					
						
							|  |  |  | 		if (replacementHref) { | 
					
						
							|  |  |  | 			const replacement = match.replace(href, replacementHref); | 
					
						
							|  |  |  | 			content = content.split(match).join(replacement); | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	}); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return content; | 
					
						
							|  |  |  | }; | 
					
						
							| 
									
										
										
										
											2024-06-13 17:05:37 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  | // eslint-disable-next-line max-len
 | 
					
						
							|  |  |  | Helpers.makeSet = (object, properties) => new Set(properties.reduce((memo, property) => memo.concat(Array.isArray(object[property]) ? object[property] : [object[property]]), [])); |