| 
									
										
										
										
											2024-01-12 15:23:30 -05:00
										 |  |  | 'use strict'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | const winston = require('winston'); | 
					
						
							| 
									
										
										
										
											2024-03-22 15:28:01 -04:00
										 |  |  | const nconf = require('nconf'); | 
					
						
							| 
									
										
										
										
											2024-01-12 15:23:30 -05:00
										 |  |  | 
 | 
					
						
							|  |  |  | const db = require('../database'); | 
					
						
							| 
									
										
										
										
											2024-06-04 14:18:14 -04:00
										 |  |  | const batch = require('../batch'); | 
					
						
							| 
									
										
										
										
											2024-02-27 15:25:13 -05:00
										 |  |  | const meta = require('../meta'); | 
					
						
							| 
									
										
										
										
											2024-02-26 11:39:32 -05:00
										 |  |  | const privileges = require('../privileges'); | 
					
						
							| 
									
										
										
										
											2024-03-14 13:17:26 -04:00
										 |  |  | const categories = require('../categories'); | 
					
						
							| 
									
										
										
										
											2024-02-12 14:34:37 -05:00
										 |  |  | const user = require('../user'); | 
					
						
							| 
									
										
										
										
											2024-01-17 11:54:20 -05:00
										 |  |  | const topics = require('../topics'); | 
					
						
							| 
									
										
										
										
											2024-01-12 15:23:30 -05:00
										 |  |  | const posts = require('../posts'); | 
					
						
							| 
									
										
										
										
											2024-01-18 16:20:37 -05:00
										 |  |  | const utils = require('../utils'); | 
					
						
							| 
									
										
										
										
											2024-01-12 15:23:30 -05:00
										 |  |  | 
 | 
					
						
							|  |  |  | const activitypub = module.parent.exports; | 
					
						
							|  |  |  | const Notes = module.exports; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-03-25 14:55:25 -04:00
										 |  |  | async function lock(value) { | 
					
						
							|  |  |  | 	const count = await db.incrObjectField('locks', value); | 
					
						
							|  |  |  | 	return count <= 1; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | async function unlock(value) { | 
					
						
							|  |  |  | 	await db.deleteObjectField('locks', value); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-03-26 10:22:17 -04:00
										 |  |  | Notes.assert = async (uid, input, options = { skipChecks: false }) => { | 
					
						
							| 
									
										
										
										
											2024-01-16 10:44:47 -05:00
										 |  |  | 	/** | 
					
						
							| 
									
										
										
										
											2024-07-05 11:09:42 -04:00
										 |  |  | 	 * Given the id or object of any as:Note, either retrieves the full context (if resolvable), | 
					
						
							|  |  |  | 	 * or traverses up the reply chain to build a context. | 
					
						
							| 
									
										
										
										
											2024-01-16 10:44:47 -05:00
										 |  |  | 	 */ | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-06-26 14:32:48 -04:00
										 |  |  | 	if (!input) { | 
					
						
							|  |  |  | 		return null; | 
					
						
							|  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2024-03-25 14:55:25 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-06-26 14:32:48 -04:00
										 |  |  | 	const id = !activitypub.helpers.isUri(input) ? input.id : input; | 
					
						
							| 
									
										
										
										
											2024-03-25 14:55:25 -04:00
										 |  |  | 	const lockStatus = await lock(id, '[[error:activitypub.already-asserting]]'); | 
					
						
							|  |  |  | 	if (!lockStatus) { // unable to achieve lock, stop processing.
 | 
					
						
							|  |  |  | 		return null; | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-06-28 12:54:32 -04:00
										 |  |  | 	let chain; | 
					
						
							|  |  |  | 	const context = await activitypub.contexts.get(uid, id); | 
					
						
							| 
									
										
										
										
											2024-07-16 11:37:38 -04:00
										 |  |  | 	if (context.tid) { | 
					
						
							|  |  |  | 		unlock(id); | 
					
						
							|  |  |  | 		const { tid } = context; | 
					
						
							|  |  |  | 		return { tid, count: 0 }; | 
					
						
							|  |  |  | 	} else if (context.context) { | 
					
						
							| 
									
										
										
										
											2024-07-19 11:06:19 -04:00
										 |  |  | 		chain = Array.from(await activitypub.contexts.getItems(uid, context.context, { input })); | 
					
						
							| 
									
										
										
										
											2024-06-28 12:54:32 -04:00
										 |  |  | 	} else { | 
					
						
							|  |  |  | 		// Fall back to inReplyTo traversal
 | 
					
						
							|  |  |  | 		chain = Array.from(await Notes.getParentChain(uid, input)); | 
					
						
							|  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2024-02-12 15:25:49 -05:00
										 |  |  | 	if (!chain.length) { | 
					
						
							| 
									
										
										
										
											2024-03-25 14:55:25 -04:00
										 |  |  | 		unlock(id); | 
					
						
							| 
									
										
										
										
											2024-02-12 15:25:49 -05:00
										 |  |  | 		return null; | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-06-28 12:54:32 -04:00
										 |  |  | 	// Reorder chain items by timestamp
 | 
					
						
							|  |  |  | 	chain = chain.sort((a, b) => a.timestamp - b.timestamp); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	const mainPost = chain[0]; | 
					
						
							| 
									
										
										
										
											2024-06-13 17:06:58 -04:00
										 |  |  | 	let { pid: mainPid, tid, uid: authorId, timestamp, name, content, _activitypub } = mainPost; | 
					
						
							| 
									
										
										
										
											2024-02-28 13:29:21 -05:00
										 |  |  | 	const hasTid = !!tid; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-06-25 11:04:40 -04:00
										 |  |  | 	const cid = hasTid ? await topics.getTopicField(tid, 'cid') : options.cid || -1; | 
					
						
							|  |  |  | 	if (options.cid && cid === -1) { | 
					
						
							|  |  |  | 		// Move topic if currently uncategorized
 | 
					
						
							|  |  |  | 		await topics.tools.move(tid, { cid: options.cid, uid: 'system' }); | 
					
						
							| 
									
										
										
										
											2024-06-13 14:02:26 -04:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-19 10:50:14 -04:00
										 |  |  | 	const members = await db.isSortedSetMembers(`tid:${tid}:posts`, chain.slice(1).map(p => p.pid)); | 
					
						
							| 
									
										
										
										
											2024-06-28 16:28:34 -04:00
										 |  |  | 	members.unshift(await posts.exists(mainPid)); | 
					
						
							| 
									
										
										
										
											2024-01-19 11:31:04 -05:00
										 |  |  | 	if (tid && members.every(Boolean)) { | 
					
						
							| 
									
										
										
										
											2024-01-16 10:44:47 -05:00
										 |  |  | 		// All cached, return early.
 | 
					
						
							| 
									
										
										
										
											2024-06-07 11:56:58 -04:00
										 |  |  | 		// winston.verbose('[notes/assert] No new notes to process.');
 | 
					
						
							| 
									
										
										
										
											2024-03-25 14:55:25 -04:00
										 |  |  | 		unlock(id); | 
					
						
							| 
									
										
										
										
											2024-05-07 10:40:47 +02:00
										 |  |  | 		return { tid, count: 0 }; | 
					
						
							| 
									
										
										
										
											2024-01-16 10:44:47 -05:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-28 13:29:21 -05:00
										 |  |  | 	let title; | 
					
						
							|  |  |  | 	if (hasTid) { | 
					
						
							| 
									
										
										
										
											2024-06-13 14:02:26 -04:00
										 |  |  | 		mainPid = await topics.getTopicField(tid, 'mainPid'); | 
					
						
							| 
									
										
										
										
											2024-02-28 13:29:21 -05:00
										 |  |  | 	} else { | 
					
						
							| 
									
										
										
										
											2024-06-13 17:06:58 -04:00
										 |  |  | 		// Check recipients/audience for local category
 | 
					
						
							|  |  |  | 		const set = activitypub.helpers.makeSet(_activitypub, ['to', 'cc', 'audience']); | 
					
						
							|  |  |  | 		const resolved = await Promise.all(Array.from(set).map(async id => await activitypub.helpers.resolveLocalId(id))); | 
					
						
							|  |  |  | 		const recipientCids = resolved | 
					
						
							|  |  |  | 			.filter(Boolean) | 
					
						
							|  |  |  | 			.filter(({ type }) => type === 'category') | 
					
						
							|  |  |  | 			.map(obj => obj.id); | 
					
						
							|  |  |  | 		if (recipientCids.length) { | 
					
						
							| 
									
										
										
										
											2024-06-14 10:20:50 -04:00
										 |  |  | 			// Overrides passed-in value, respect addressing from main post over booster
 | 
					
						
							|  |  |  | 			options.cid = recipientCids.shift(); | 
					
						
							| 
									
										
										
										
											2024-06-13 17:06:58 -04:00
										 |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-28 13:29:21 -05:00
										 |  |  | 		// mainPid ok to leave as-is
 | 
					
						
							| 
									
										
										
										
											2024-04-29 16:16:07 -04:00
										 |  |  | 		title = name || activitypub.helpers.generateTitle(utils.decodeHTMLEntities(content)); | 
					
						
							| 
									
										
										
										
											2024-02-28 13:29:21 -05:00
										 |  |  | 	} | 
					
						
							| 
									
										
										
										
											2024-02-29 00:06:59 -05:00
										 |  |  | 	mainPid = utils.isNumber(mainPid) ? parseInt(mainPid, 10) : mainPid; | 
					
						
							| 
									
										
										
										
											2024-02-26 11:39:32 -05:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-03-26 10:22:17 -04:00
										 |  |  | 	// Relation & privilege check for local categories
 | 
					
						
							| 
									
										
										
										
											2024-05-06 15:54:45 -04:00
										 |  |  | 	const hasRelation = uid || options.skipChecks || options.cid || hasTid || await assertRelation(chain[0]); | 
					
						
							| 
									
										
										
										
											2024-04-16 13:38:05 -04:00
										 |  |  | 	const privilege = `topics:${tid ? 'reply' : 'create'}`; | 
					
						
							| 
									
										
										
										
											2024-02-26 11:39:32 -05:00
										 |  |  | 	const allowed = await privileges.categories.can(privilege, cid, activitypub._constants.uid); | 
					
						
							| 
									
										
										
										
											2024-03-26 10:22:17 -04:00
										 |  |  | 	if (!hasRelation || !allowed) { | 
					
						
							|  |  |  | 		if (!hasRelation) { | 
					
						
							|  |  |  | 			winston.info(`[activitypub/notes.assert] Not asserting ${id} as it has no relation to existing tracked content.`); | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-03-25 14:55:25 -04:00
										 |  |  | 		unlock(id); | 
					
						
							| 
									
										
										
										
											2024-02-26 11:39:32 -05:00
										 |  |  | 		return null; | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-27 15:25:13 -05:00
										 |  |  | 	tid = tid || utils.generateUUID(); | 
					
						
							| 
									
										
										
										
											2024-02-29 11:19:56 -05:00
										 |  |  | 	mainPost.tid = tid; | 
					
						
							| 
									
										
										
										
											2024-02-27 15:25:13 -05:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-03-05 14:24:13 -05:00
										 |  |  | 	const unprocessed = chain.map((post) => { | 
					
						
							|  |  |  | 		post.tid = tid; // add tid to post hash
 | 
					
						
							|  |  |  | 		return post; | 
					
						
							|  |  |  | 	}).filter((p, idx) => !members[idx]); | 
					
						
							| 
									
										
										
										
											2024-03-13 11:03:08 -04:00
										 |  |  | 	const count = unprocessed.length; | 
					
						
							| 
									
										
										
										
											2024-06-07 11:56:58 -04:00
										 |  |  | 	// winston.verbose(`[notes/assert] ${count} new note(s) found.`);
 | 
					
						
							| 
									
										
										
										
											2024-01-16 10:44:47 -05:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-29 11:19:56 -05:00
										 |  |  | 	let tags; | 
					
						
							|  |  |  | 	if (!hasTid) { | 
					
						
							| 
									
										
										
										
											2024-03-15 16:38:00 -04:00
										 |  |  | 		const { to, cc, attachment } = mainPost._activitypub; | 
					
						
							| 
									
										
										
										
											2024-03-14 13:17:26 -04:00
										 |  |  | 		const systemTags = (meta.config.systemTags || '').split(','); | 
					
						
							| 
									
										
										
										
											2024-03-14 14:48:35 -04:00
										 |  |  | 		const maxTags = await categories.getCategoryField(cid, 'maxTags'); | 
					
						
							| 
									
										
										
										
											2024-03-07 15:11:43 -05:00
										 |  |  | 		tags = (mainPost._activitypub.tag || []) | 
					
						
							| 
									
										
										
										
											2024-03-14 13:41:04 -04:00
										 |  |  | 			.filter(o => o.type === 'Hashtag' && !systemTags.includes(o.name.slice(1))) | 
					
						
							| 
									
										
										
										
											2024-03-07 15:11:43 -05:00
										 |  |  | 			.map(o => o.name.slice(1)); | 
					
						
							| 
									
										
										
										
											2024-03-12 13:27:29 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-06-28 12:54:32 -04:00
										 |  |  | 		if (tags.length > maxTags) { | 
					
						
							| 
									
										
										
										
											2024-03-14 13:17:26 -04:00
										 |  |  | 			tags.length = maxTags; | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-03-15 16:38:00 -04:00
										 |  |  | 		await Promise.all([ | 
					
						
							|  |  |  | 			topics.post({ | 
					
						
							|  |  |  | 				tid, | 
					
						
							|  |  |  | 				uid: authorId, | 
					
						
							|  |  |  | 				cid, | 
					
						
							|  |  |  | 				pid: mainPid, | 
					
						
							|  |  |  | 				title, | 
					
						
							|  |  |  | 				timestamp, | 
					
						
							|  |  |  | 				tags, | 
					
						
							|  |  |  | 				content: mainPost.content, | 
					
						
							|  |  |  | 				_activitypub: mainPost._activitypub, | 
					
						
							|  |  |  | 			}), | 
					
						
							|  |  |  | 			Notes.updateLocalRecipients(mainPid, { to, cc }), | 
					
						
							| 
									
										
										
										
											2024-04-10 22:01:44 -04:00
										 |  |  | 			posts.attachments.update(mainPid, attachment), | 
					
						
							| 
									
										
										
										
											2024-03-15 16:38:00 -04:00
										 |  |  | 		]); | 
					
						
							| 
									
										
										
										
											2024-06-28 12:54:32 -04:00
										 |  |  | 		unprocessed.shift(); | 
					
						
							| 
									
										
										
										
											2024-02-29 11:19:56 -05:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-03-12 13:27:29 -04:00
										 |  |  | 	for (const post of unprocessed) { | 
					
						
							| 
									
										
										
										
											2024-03-15 16:38:00 -04:00
										 |  |  | 		const { to, cc, attachment } = post._activitypub; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-03-12 13:27:29 -04:00
										 |  |  | 		// eslint-disable-next-line no-await-in-loop
 | 
					
						
							| 
									
										
										
										
											2024-03-15 16:38:00 -04:00
										 |  |  | 		await Promise.all([ | 
					
						
							|  |  |  | 			topics.reply(post), | 
					
						
							|  |  |  | 			Notes.updateLocalRecipients(post.pid, { to, cc }), | 
					
						
							| 
									
										
										
										
											2024-04-10 22:01:44 -04:00
										 |  |  | 			posts.attachments.update(post.pid, attachment), | 
					
						
							| 
									
										
										
										
											2024-03-15 16:38:00 -04:00
										 |  |  | 		]); | 
					
						
							| 
									
										
										
										
											2024-03-12 13:27:29 -04:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-03-25 14:55:25 -04:00
										 |  |  | 	await Promise.all([ | 
					
						
							| 
									
										
										
										
											2024-05-06 15:54:45 -04:00
										 |  |  | 		Notes.syncUserInboxes(tid, uid), | 
					
						
							| 
									
										
										
										
											2024-03-25 14:55:25 -04:00
										 |  |  | 		unlock(id), | 
					
						
							|  |  |  | 	]); | 
					
						
							| 
									
										
										
										
											2024-01-12 15:23:30 -05:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-03-13 11:03:08 -04:00
										 |  |  | 	return { tid, count }; | 
					
						
							| 
									
										
										
										
											2024-01-12 15:23:30 -05:00
										 |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-03-26 10:22:17 -04:00
										 |  |  | async function assertRelation(post) { | 
					
						
							|  |  |  | 	/** | 
					
						
							|  |  |  | 	 * Given a mocked post object, ensures that it is related to some other object in database | 
					
						
							|  |  |  | 	 * This check ensures that random content isn't added to the database just because it is received. | 
					
						
							|  |  |  | 	 */ | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Is followed by at least one local user
 | 
					
						
							| 
									
										
										
										
											2024-04-16 14:00:01 -04:00
										 |  |  | 	const numFollowers = await activitypub.actors.getLocalFollowersCount(post.uid); | 
					
						
							| 
									
										
										
										
											2024-03-26 10:22:17 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	// Local user is mentioned
 | 
					
						
							|  |  |  | 	const { tag } = post._activitypub; | 
					
						
							|  |  |  | 	let uids = []; | 
					
						
							| 
									
										
										
										
											2024-04-03 13:49:27 -04:00
										 |  |  | 	if (tag && tag.length) { | 
					
						
							| 
									
										
										
										
											2024-03-26 10:22:17 -04:00
										 |  |  | 		const slugs = tag.reduce((slugs, tag) => { | 
					
						
							|  |  |  | 			if (tag.type === 'Mention') { | 
					
						
							|  |  |  | 				const [slug, hostname] = tag.name.slice(1).split('@'); | 
					
						
							|  |  |  | 				if (hostname === nconf.get('url_parsed').hostname) { | 
					
						
							|  |  |  | 					slugs.push(slug); | 
					
						
							|  |  |  | 				} | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 			return slugs; | 
					
						
							|  |  |  | 		}, []); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		uids = slugs.length ? await db.sortedSetScores('userslug:uid', slugs) : []; | 
					
						
							|  |  |  | 		uids = uids.filter(Boolean); | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-16 14:00:01 -04:00
										 |  |  | 	return numFollowers > 0 || uids.length; | 
					
						
							| 
									
										
										
										
											2024-03-26 10:22:17 -04:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-03-22 14:54:53 -04:00
										 |  |  | Notes.updateLocalRecipients = async (id, { to, cc }) => { | 
					
						
							|  |  |  | 	const recipients = new Set([...(to || []), ...(cc || [])]); | 
					
						
							|  |  |  | 	const uids = new Set(); | 
					
						
							|  |  |  | 	await Promise.all(Array.from(recipients).map(async (recipient) => { | 
					
						
							|  |  |  | 		const { type, id } = await activitypub.helpers.resolveLocalId(recipient); | 
					
						
							|  |  |  | 		if (type === 'user' && await user.exists(id)) { | 
					
						
							|  |  |  | 			uids.add(parseInt(id, 10)); | 
					
						
							|  |  |  | 			return; | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		const followedUid = await db.getObjectField('followersUrl:uid', recipient); | 
					
						
							|  |  |  | 		if (followedUid) { | 
					
						
							| 
									
										
										
										
											2024-04-16 14:00:01 -04:00
										 |  |  | 			const { uids: followers } = await activitypub.actors.getLocalFollowers(followedUid); | 
					
						
							|  |  |  | 			if (followers.size > 0) { | 
					
						
							|  |  |  | 				followers.forEach((uid) => { | 
					
						
							|  |  |  | 					uids.add(uid); | 
					
						
							|  |  |  | 				}); | 
					
						
							| 
									
										
										
										
											2024-03-22 14:54:53 -04:00
										 |  |  | 			} | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	})); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	if (uids.size > 0) { | 
					
						
							|  |  |  | 		await db.setAdd(`post:${id}:recipients`, Array.from(uids)); | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | Notes.getParentChain = async (uid, input) => { | 
					
						
							|  |  |  | 	// Traverse upwards via `inReplyTo` until you find the root-level Note
 | 
					
						
							|  |  |  | 	const id = activitypub.helpers.isUri(input) ? input : input.id; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	const chain = new Set(); | 
					
						
							|  |  |  | 	const traverse = async (uid, id) => { | 
					
						
							|  |  |  | 		// Handle remote reference to local post
 | 
					
						
							|  |  |  | 		const { type, id: localId } = await activitypub.helpers.resolveLocalId(id); | 
					
						
							|  |  |  | 		if (type === 'post' && localId) { | 
					
						
							|  |  |  | 			return await traverse(uid, localId); | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-06-12 16:04:14 -04:00
										 |  |  | 		const postData = await posts.getPostData(id); | 
					
						
							|  |  |  | 		if (postData) { | 
					
						
							| 
									
										
										
										
											2024-03-22 14:54:53 -04:00
										 |  |  | 			chain.add(postData); | 
					
						
							|  |  |  | 			if (postData.toPid) { | 
					
						
							|  |  |  | 				await traverse(uid, postData.toPid); | 
					
						
							|  |  |  | 			} else if (utils.isNumber(id)) { // local pid without toPid, could be OP or reply to OP
 | 
					
						
							|  |  |  | 				const mainPid = await topics.getTopicField(postData.tid, 'mainPid'); | 
					
						
							|  |  |  | 				if (mainPid !== parseInt(id, 10)) { | 
					
						
							|  |  |  | 					await traverse(uid, mainPid); | 
					
						
							|  |  |  | 				} | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		} else { | 
					
						
							| 
									
										
										
										
											2024-03-22 15:28:01 -04:00
										 |  |  | 			let object = !activitypub.helpers.isUri(input) && input.id === id ? input : undefined; | 
					
						
							| 
									
										
										
										
											2024-03-22 14:54:53 -04:00
										 |  |  | 			try { | 
					
						
							| 
									
										
										
										
											2024-03-22 15:28:01 -04:00
										 |  |  | 				object = object || await activitypub.get('uid', uid, id); | 
					
						
							| 
									
										
										
										
											2024-03-22 14:54:53 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  | 				// Handle incorrect id passed in
 | 
					
						
							|  |  |  | 				if (id !== object.id) { | 
					
						
							|  |  |  | 					return await traverse(uid, object.id); | 
					
						
							|  |  |  | 				} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				object = await activitypub.mocks.post(object); | 
					
						
							|  |  |  | 				if (object) { | 
					
						
							|  |  |  | 					chain.add(object); | 
					
						
							|  |  |  | 					if (object.toPid) { | 
					
						
							|  |  |  | 						await traverse(uid, object.toPid); | 
					
						
							|  |  |  | 					} | 
					
						
							|  |  |  | 				} | 
					
						
							|  |  |  | 			} catch (e) { | 
					
						
							| 
									
										
										
										
											2024-06-14 13:26:56 -04:00
										 |  |  | 				winston.verbose(`[activitypub/notes/getParentChain] Cannot retrieve ${id}, terminating here.`); | 
					
						
							| 
									
										
										
										
											2024-03-22 14:54:53 -04:00
										 |  |  | 			} | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	}; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	await traverse(uid, id); | 
					
						
							|  |  |  | 	return chain; | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-05-06 15:54:45 -04:00
										 |  |  | Notes.syncUserInboxes = async function (tid, uid) { | 
					
						
							| 
									
										
										
										
											2024-02-12 14:51:21 -05:00
										 |  |  | 	const [pids, { cid, mainPid }] = await Promise.all([ | 
					
						
							|  |  |  | 		db.getSortedSetMembers(`tid:${tid}:posts`), | 
					
						
							| 
									
										
										
										
											2024-02-12 14:59:13 -05:00
										 |  |  | 		topics.getTopicFields(tid, ['tid', 'cid', 'mainPid']), | 
					
						
							| 
									
										
										
										
											2024-02-12 14:51:21 -05:00
										 |  |  | 	]); | 
					
						
							|  |  |  | 	pids.unshift(mainPid); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-03-26 16:41:51 -04:00
										 |  |  | 	const recipients = await db.getSetsMembers(pids.map(id => `post:${id}:recipients`)); | 
					
						
							|  |  |  | 	const uids = recipients.reduce((set, uids) => new Set([...set, ...uids.map(u => parseInt(u, 10))]), new Set()); | 
					
						
							| 
									
										
										
										
											2024-05-06 15:54:45 -04:00
										 |  |  | 	if (uid) { | 
					
						
							|  |  |  | 		uids.add(parseInt(uid, 10)); | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-03-26 16:41:51 -04:00
										 |  |  | 	const keys = Array.from(uids).map(uid => `uid:${uid}:inbox`); | 
					
						
							| 
									
										
										
										
											2024-02-12 14:34:37 -05:00
										 |  |  | 	const score = await db.sortedSetScore(`cid:${cid}:tids`, tid); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-05 12:00:19 -04:00
										 |  |  | 	const removeKeys = (await db.getSetMembers(`tid:${tid}:recipients`)) | 
					
						
							|  |  |  | 		.filter(uid => !uids.has(parseInt(uid, 10))) | 
					
						
							|  |  |  | 		.map((uid => `uid:${uid}:inbox`)); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-06-07 11:56:58 -04:00
										 |  |  | 	// winston.verbose(`[activitypub/syncUserInboxes] Syncing tid ${tid} with ${uids.size} inboxes`);
 | 
					
						
							| 
									
										
										
										
											2024-05-09 15:48:58 -04:00
										 |  |  | 	await Promise.all([ | 
					
						
							| 
									
										
										
										
											2024-07-05 12:00:19 -04:00
										 |  |  | 		db.sortedSetsRemove(removeKeys, tid), | 
					
						
							| 
									
										
										
										
											2024-05-09 15:48:58 -04:00
										 |  |  | 		db.sortedSetsAdd(keys, keys.map(() => score || Date.now()), tid), | 
					
						
							|  |  |  | 		db.setAdd(`tid:${tid}:recipients`, Array.from(uids)), | 
					
						
							|  |  |  | 	]); | 
					
						
							| 
									
										
										
										
											2024-02-12 14:34:37 -05:00
										 |  |  | }; | 
					
						
							| 
									
										
										
										
											2024-03-22 14:39:18 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  | Notes.getCategoryFollowers = async (cid) => { | 
					
						
							|  |  |  | 	// Retrieves remote users who have followed a category; used to build recipient list
 | 
					
						
							|  |  |  | 	let uids = await db.getSortedSetRangeByScore(`cid:${cid}:uid:watch:state`, 0, -1, categories.watchStates.tracking, categories.watchStates.tracking); | 
					
						
							|  |  |  | 	uids = uids.filter(uid => !utils.isNumber(uid)); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return uids; | 
					
						
							|  |  |  | }; | 
					
						
							| 
									
										
										
										
											2024-05-01 14:44:29 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  | Notes.announce = {}; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | Notes.announce.list = async ({ pid, tid }) => { | 
					
						
							|  |  |  | 	let pids = []; | 
					
						
							|  |  |  | 	if (pid) { | 
					
						
							|  |  |  | 		pids = [pid]; | 
					
						
							|  |  |  | 	} else if (tid) { | 
					
						
							|  |  |  | 		let mainPid; | 
					
						
							|  |  |  | 		([pids, mainPid] = await Promise.all([ | 
					
						
							|  |  |  | 			db.getSortedSetMembers(`tid:${tid}:posts`), | 
					
						
							|  |  |  | 			topics.getTopicField(tid, 'mainPid'), | 
					
						
							|  |  |  | 		])); | 
					
						
							|  |  |  | 		pids.unshift(mainPid); | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	if (!pids.length) { | 
					
						
							|  |  |  | 		return []; | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	const keys = pids.map(pid => `pid:${pid}:announces`); | 
					
						
							|  |  |  | 	let announces = await db.getSortedSetsMembersWithScores(keys); | 
					
						
							|  |  |  | 	announces = announces.reduce((memo, cur, idx) => { | 
					
						
							|  |  |  | 		if (cur.length) { | 
					
						
							|  |  |  | 			const pid = pids[idx]; | 
					
						
							|  |  |  | 			cur.forEach(({ value: actor, score: timestamp }) => { | 
					
						
							|  |  |  | 				memo.push({ pid, actor, timestamp }); | 
					
						
							|  |  |  | 			}); | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		return memo; | 
					
						
							|  |  |  | 	}, []); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return announces; | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | Notes.announce.add = async (pid, actor, timestamp = Date.now()) => { | 
					
						
							|  |  |  | 	await db.sortedSetAdd(`pid:${pid}:announces`, timestamp, actor); | 
					
						
							| 
									
										
										
										
											2024-06-17 11:18:48 -04:00
										 |  |  | 	await posts.setPostField(pid, 'announces', await db.sortedSetCard(`pid:${pid}:announces`)); | 
					
						
							| 
									
										
										
										
											2024-05-01 14:44:29 -04:00
										 |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | Notes.announce.remove = async (pid, actor) => { | 
					
						
							|  |  |  | 	await db.sortedSetRemove(`pid:${pid}:announces`, actor); | 
					
						
							| 
									
										
										
										
											2024-06-17 11:18:48 -04:00
										 |  |  | 	const count = await db.sortedSetCard(`pid:${pid}:announces`); | 
					
						
							|  |  |  | 	if (count > 0) { | 
					
						
							|  |  |  | 		await posts.setPostField(pid, 'announces', count); | 
					
						
							|  |  |  | 	} else { | 
					
						
							|  |  |  | 		await db.deleteObjectField(`post:${pid}`, 'announces'); | 
					
						
							|  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2024-05-01 14:44:29 -04:00
										 |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | Notes.announce.removeAll = async (pid) => { | 
					
						
							| 
									
										
										
										
											2024-06-17 11:18:48 -04:00
										 |  |  | 	await Promise.all([ | 
					
						
							|  |  |  | 		db.delete(`pid:${pid}:announces`), | 
					
						
							|  |  |  | 		db.deleteObjectField(`post:${pid}`, 'announces'), | 
					
						
							|  |  |  | 	]); | 
					
						
							| 
									
										
										
										
											2024-05-01 14:44:29 -04:00
										 |  |  | }; | 
					
						
							| 
									
										
										
										
											2024-05-09 15:48:58 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  | Notes.delete = async (pids) => { | 
					
						
							|  |  |  | 	if (!Array.isArray(pids)) { | 
					
						
							|  |  |  | 		pids = [pids]; | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	const exists = await posts.exists(pids); | 
					
						
							|  |  |  | 	pids = pids.filter((_, idx) => exists[idx]); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-05 12:00:19 -04:00
										 |  |  | 	let tids = await posts.getPostsFields(pids, ['tid']); | 
					
						
							|  |  |  | 	tids = new Set(tids.map(obj => obj.tid)); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-05-09 15:48:58 -04:00
										 |  |  | 	const recipientSets = pids.map(id => `post:${id}:recipients`); | 
					
						
							| 
									
										
										
										
											2024-05-10 14:40:04 -04:00
										 |  |  | 	const announcerSets = pids.map(id => `pid:${id}:announces`); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	await db.deleteAll([...recipientSets, ...announcerSets]); | 
					
						
							| 
									
										
										
										
											2024-07-05 12:00:19 -04:00
										 |  |  | 	await Promise.all(Array.from(tids).map(async tid => Notes.syncUserInboxes(tid))); | 
					
						
							| 
									
										
										
										
											2024-05-09 15:48:58 -04:00
										 |  |  | }; | 
					
						
							| 
									
										
										
										
											2024-06-04 14:18:14 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  | Notes.prune = async () => { | 
					
						
							|  |  |  | 	/** | 
					
						
							|  |  |  | 	 * Prune topics in cid -1 that have received no engagement. | 
					
						
							|  |  |  | 	 * Engagement is defined as: | 
					
						
							|  |  |  | 	 *   - Replied to (contains a local reply) | 
					
						
							|  |  |  | 	 *   - Post within is liked | 
					
						
							|  |  |  | 	 */ | 
					
						
							| 
									
										
										
										
											2024-06-07 12:54:11 -04:00
										 |  |  | 	winston.info('[notes/prune] Starting scheduled pruning of topics'); | 
					
						
							| 
									
										
										
										
											2024-06-14 07:44:10 -04:00
										 |  |  | 	const start = '-inf'; | 
					
						
							| 
									
										
										
										
											2024-06-04 14:18:14 -04:00
										 |  |  | 	const stop = Date.now() - (1000 * 60 * 60 * 24 * 30); // 30 days; todo: make configurable?
 | 
					
						
							|  |  |  | 	let tids = await db.getSortedSetRangeByScore('cid:-1:tids', 0, -1, start, stop); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-06-07 12:54:11 -04:00
										 |  |  | 	winston.info(`[notes/prune] Found ${tids.length} topics older than 30 days (since last activity).`); | 
					
						
							| 
									
										
										
										
											2024-06-04 14:18:14 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	const posters = await db.getSortedSetsMembers(tids.map(tid => `tid:${tid}:posters`)); | 
					
						
							|  |  |  | 	const hasLocalVoter = await Promise.all(tids.map(async (tid) => { | 
					
						
							|  |  |  | 		const mainPid = await db.getObjectField(`topic:${tid}`, 'mainPid'); | 
					
						
							|  |  |  | 		const pids = await db.getSortedSetMembers(`tid:${tid}:posts`); | 
					
						
							|  |  |  | 		pids.unshift(mainPid); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// Check voters of each pid for a local uid
 | 
					
						
							|  |  |  | 		const voters = new Set(); | 
					
						
							|  |  |  | 		await Promise.all(pids.map(async (pid) => { | 
					
						
							|  |  |  | 			const [upvoters, downvoters] = await db.getSetsMembers([`pid:${pid}:upvote`, `pid:${pid}:downvote`]); | 
					
						
							|  |  |  | 			upvoters.forEach(uid => voters.add(uid)); | 
					
						
							|  |  |  | 			downvoters.forEach(uid => voters.add(uid)); | 
					
						
							|  |  |  | 		})); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		return Array.from(voters).some(uid => utils.isNumber(uid)); | 
					
						
							|  |  |  | 	})); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	tids = tids.filter((_, idx) => { | 
					
						
							|  |  |  | 		const localPoster = posters[idx].some(uid => utils.isNumber(uid)); | 
					
						
							|  |  |  | 		const localVoter = hasLocalVoter[idx]; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		return !localPoster && !localVoter; | 
					
						
							|  |  |  | 	}); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-06-07 12:54:11 -04:00
										 |  |  | 	winston.info(`[notes/prune] ${tids.length} topics eligible for pruning`); | 
					
						
							| 
									
										
										
										
											2024-06-04 14:18:14 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	await batch.processArray(tids, async (tids) => { | 
					
						
							| 
									
										
										
										
											2024-06-07 16:48:05 -04:00
										 |  |  | 		await Promise.all(tids.map(async tid => await topics.purgePostsAndTopic(tid, 0))); | 
					
						
							| 
									
										
										
										
											2024-06-04 14:18:14 -04:00
										 |  |  | 	}, { batch: 100 }); | 
					
						
							| 
									
										
										
										
											2024-06-10 12:30:44 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	winston.info('[notes/prune] Scheduled pruning of topics complete.'); | 
					
						
							| 
									
										
										
										
											2024-06-04 14:18:14 -04:00
										 |  |  | }; |