| 
									
										
										
										
											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'); | 
					
						
							| 
									
										
										
										
											2025-08-28 14:12:04 -04:00
										 |  |  | const tokenizer = require('sbd'); | 
					
						
							| 
									
										
										
										
											2025-09-05 13:11:52 -04:00
										 |  |  | const pretty = require('pretty'); | 
					
						
							| 
									
										
										
										
											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-10-08 14:26:55 -04:00
										 |  |  | const messaging = require('../messaging'); | 
					
						
							| 
									
										
										
										
											2025-03-07 15:26:00 -05:00
										 |  |  | const notifications = require('../notifications'); | 
					
						
							| 
									
										
										
										
											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; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-05-06 10:44:47 -04:00
										 |  |  | Notes._normalizeTags = async (tag, cid) => { | 
					
						
							|  |  |  | 	const systemTags = (meta.config.systemTags || '').split(','); | 
					
						
							|  |  |  | 	const maxTags = await categories.getCategoryField(cid, 'maxTags'); | 
					
						
							| 
									
										
										
										
											2025-09-15 14:10:02 -04:00
										 |  |  | 	let tags = tag || []; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	if (!Array.isArray(tags)) { // the "|| []" should handle null/undefined values... #famouslastwords
 | 
					
						
							|  |  |  | 		tags = [tags]; | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	tags = tags | 
					
						
							| 
									
										
										
										
											2025-09-22 11:56:55 -04:00
										 |  |  | 		.filter(({ type }) => type === 'Hashtag') | 
					
						
							| 
									
										
										
										
											2025-05-06 10:44:47 -04:00
										 |  |  | 		.map((tag) => { | 
					
						
							|  |  |  | 			tag.name = tag.name.startsWith('#') ? tag.name.slice(1) : tag.name; | 
					
						
							|  |  |  | 			return tag; | 
					
						
							|  |  |  | 		}) | 
					
						
							| 
									
										
										
										
											2025-09-22 11:56:55 -04:00
										 |  |  | 		.filter(({ name }) => !systemTags.includes(name)) | 
					
						
							| 
									
										
										
										
											2025-05-06 10:44:47 -04:00
										 |  |  | 		.map(t => t.name); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	if (tags.length > maxTags) { | 
					
						
							|  |  |  | 		tags.length = maxTags; | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return tags; | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											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; | 
					
						
							|  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2025-08-27 13:28:26 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-29 13:33:14 -04:00
										 |  |  | 	let id = !activitypub.helpers.isUri(input) ? input.id : input; | 
					
						
							| 
									
										
										
										
											2025-09-19 10:34:57 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	let lockStatus = await db.incrObjectField('locks', id); | 
					
						
							|  |  |  | 	lockStatus = lockStatus <= 1; | 
					
						
							| 
									
										
										
										
											2024-03-25 14:55:25 -04:00
										 |  |  | 	if (!lockStatus) { // unable to achieve lock, stop processing.
 | 
					
						
							| 
									
										
										
										
											2025-08-30 13:24:33 -04:00
										 |  |  | 		winston.warn(`[activitypub/notes.assert] Unable to acquire lock, skipping processing of ${id}`); | 
					
						
							| 
									
										
										
										
											2024-03-25 14:55:25 -04:00
										 |  |  | 		return null; | 
					
						
							|  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2025-08-27 13:28:26 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-11 10:30:21 -04:00
										 |  |  | 	try { | 
					
						
							| 
									
										
										
										
											2025-09-25 11:56:38 -04:00
										 |  |  | 		if (!(options.skipChecks || process.env.hasOwnProperty('CI'))) { | 
					
						
							| 
									
										
										
										
											2025-09-11 10:30:21 -04:00
										 |  |  | 			id = (await activitypub.checkHeader(id)) || id; | 
					
						
							| 
									
										
										
										
											2024-10-24 14:35:34 -04:00
										 |  |  | 		} | 
					
						
							| 
									
										
										
										
											2025-08-27 13:28:26 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-11 10:30:21 -04:00
										 |  |  | 		let chain; | 
					
						
							|  |  |  | 		let context = await activitypub.contexts.get(uid, id); | 
					
						
							|  |  |  | 		if (context.tid) { | 
					
						
							|  |  |  | 			const { tid } = context; | 
					
						
							|  |  |  | 			return { tid, count: 0 }; | 
					
						
							|  |  |  | 		} else if (context.context) { | 
					
						
							|  |  |  | 			chain = Array.from(await activitypub.contexts.getItems(uid, context.context, { input })); | 
					
						
							|  |  |  | 			if (chain && chain.length) { | 
					
						
							|  |  |  | 				// Context resolves, use in later topic creation
 | 
					
						
							|  |  |  | 				context = context.context; | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		} else { | 
					
						
							|  |  |  | 			context = undefined; | 
					
						
							|  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2024-02-12 15:25:49 -05:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-11 10:30:21 -04:00
										 |  |  | 		if (!chain || !chain.length) { | 
					
						
							|  |  |  | 			// Fall back to inReplyTo traversal on context retrieval failure
 | 
					
						
							|  |  |  | 			chain = Array.from(await Notes.getParentChain(uid, input)); | 
					
						
							|  |  |  | 			chain.reverse(); | 
					
						
							|  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2024-06-28 12:54:32 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-11 10:30:21 -04:00
										 |  |  | 		// Can't resolve — give up.
 | 
					
						
							|  |  |  | 		if (!chain.length) { | 
					
						
							|  |  |  | 			return null; | 
					
						
							|  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2024-02-28 13:29:21 -05:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-11 10:30:21 -04:00
										 |  |  | 		// Reorder chain items by timestamp
 | 
					
						
							|  |  |  | 		chain = chain.sort((a, b) => a.timestamp - b.timestamp); | 
					
						
							| 
									
										
										
										
											2025-08-27 13:28:26 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-11 10:30:21 -04:00
										 |  |  | 		const mainPost = chain[0]; | 
					
						
							|  |  |  | 		let { pid: mainPid, tid, uid: authorId, timestamp, title, content, sourceContent, _activitypub } = mainPost; | 
					
						
							|  |  |  | 		const hasTid = !!tid; | 
					
						
							| 
									
										
										
										
											2025-08-27 13:28:26 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-11 10:30:21 -04:00
										 |  |  | 		const cid = hasTid ? await topics.getTopicField(tid, 'cid') : options.cid || -1; | 
					
						
							| 
									
										
										
										
											2025-08-27 13:28:26 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-11 10:30:21 -04:00
										 |  |  | 		if (options.cid && cid === -1) { | 
					
						
							|  |  |  | 			// Move topic if currently uncategorized
 | 
					
						
							|  |  |  | 			await topics.tools.move(tid, { cid: options.cid, uid: 'system' }); | 
					
						
							| 
									
										
										
										
											2025-05-15 15:38:57 -04:00
										 |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-22 12:14:14 -04:00
										 |  |  | 		const exists = await posts.exists(chain.map(p => p.pid)); | 
					
						
							|  |  |  | 		if (tid && exists.every(Boolean)) { | 
					
						
							| 
									
										
										
										
											2025-09-11 10:30:21 -04:00
										 |  |  | 			// All cached, return early.
 | 
					
						
							|  |  |  | 			activitypub.helpers.log('[notes/assert] No new notes to process.'); | 
					
						
							|  |  |  | 			return { tid, count: 0 }; | 
					
						
							| 
									
										
										
										
											2024-06-13 17:06:58 -04:00
										 |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-11 10:30:21 -04:00
										 |  |  | 		if (hasTid) { | 
					
						
							|  |  |  | 			mainPid = await topics.getTopicField(tid, 'mainPid'); | 
					
						
							|  |  |  | 		} else { | 
					
						
							|  |  |  | 			// Check recipients/audience for category (local or remote)
 | 
					
						
							|  |  |  | 			const set = activitypub.helpers.makeSet(_activitypub, ['to', 'cc', 'audience']); | 
					
						
							|  |  |  | 			await activitypub.actors.assert(Array.from(set)); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			// Local
 | 
					
						
							|  |  |  | 			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); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			// Remote
 | 
					
						
							|  |  |  | 			let remoteCid; | 
					
						
							|  |  |  | 			const assertedGroups = await categories.exists(Array.from(set)); | 
					
						
							|  |  |  | 			try { | 
					
						
							|  |  |  | 				const { hostname } = new URL(mainPid); | 
					
						
							|  |  |  | 				remoteCid = Array.from(set).filter((id, idx) => { | 
					
						
							|  |  |  | 					const { hostname: cidHostname } = new URL(id); | 
					
						
							|  |  |  | 					return assertedGroups[idx] && cidHostname === hostname; | 
					
						
							|  |  |  | 				}).shift(); | 
					
						
							|  |  |  | 			} catch (e) { | 
					
						
							|  |  |  | 				// noop
 | 
					
						
							|  |  |  | 				winston.error('[activitypub/notes.assert] Could not parse URL of mainPid', e.stack); | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			if (remoteCid || recipientCids.length) { | 
					
						
							|  |  |  | 				// Overrides passed-in value, respect addressing from main post over booster
 | 
					
						
							|  |  |  | 				options.cid = remoteCid || recipientCids.shift(); | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			// Auto-categorization (takes place only if all other categorization efforts fail)
 | 
					
						
							|  |  |  | 			if (!options.cid) { | 
					
						
							|  |  |  | 				options.cid = await assignCategory(mainPost); | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			// mainPid ok to leave as-is
 | 
					
						
							|  |  |  | 			if (!title) { | 
					
						
							| 
									
										
										
										
											2025-10-01 12:13:57 -04:00
										 |  |  | 				let prettified = pretty(content || sourceContent); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				// Remove any lines that contain quote-post fallbacks
 | 
					
						
							| 
									
										
										
										
											2025-10-06 22:17:35 -04:00
										 |  |  | 				prettified = prettified.split('\n').filter(line => !line.startsWith('<p class="quote-inline"')).join('\n'); | 
					
						
							| 
									
										
										
										
											2025-09-11 10:30:21 -04:00
										 |  |  | 				const sentences = tokenizer.sentences(prettified, { sanitize: true, newline_boundaries: true }); | 
					
						
							|  |  |  | 				title = sentences.shift(); | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			// Remove any custom emoji from title
 | 
					
						
							|  |  |  | 			if (_activitypub && _activitypub.tag && Array.isArray(_activitypub.tag)) { | 
					
						
							|  |  |  | 				_activitypub.tag | 
					
						
							|  |  |  | 					.filter(tag => tag.type === 'Emoji') | 
					
						
							|  |  |  | 					.forEach((tag) => { | 
					
						
							|  |  |  | 						title = title.replace(new RegExp(tag.name, 'g'), ''); | 
					
						
							|  |  |  | 					}); | 
					
						
							|  |  |  | 			} | 
					
						
							| 
									
										
										
										
											2025-08-25 11:47:01 -04:00
										 |  |  | 		} | 
					
						
							| 
									
										
										
										
											2025-09-11 10:30:21 -04:00
										 |  |  | 		mainPid = utils.isNumber(mainPid) ? parseInt(mainPid, 10) : mainPid; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// Relation & privilege check for local categories
 | 
					
						
							|  |  |  | 		const inputIndex = chain.map(n => n.pid).indexOf(id); | 
					
						
							|  |  |  | 		const hasRelation = | 
					
						
							|  |  |  | 			uid || hasTid || | 
					
						
							|  |  |  | 			options.skipChecks || options.cid || | 
					
						
							|  |  |  | 			await assertRelation(chain[inputIndex !== -1 ? inputIndex : 0]); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		const privilege = `topics:${tid ? 'reply' : 'create'}`; | 
					
						
							|  |  |  | 		const allowed = await privileges.categories.can(privilege, options.cid || cid, activitypub._constants.uid); | 
					
						
							|  |  |  | 		if (!hasRelation || !allowed) { | 
					
						
							|  |  |  | 			if (!hasRelation) { | 
					
						
							|  |  |  | 				activitypub.helpers.log(`[activitypub/notes.assert] Not asserting ${id} as it has no relation to existing tracked content.`); | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			return null; | 
					
						
							| 
									
										
										
										
											2025-08-28 14:12:04 -04:00
										 |  |  | 		} | 
					
						
							| 
									
										
										
										
											2024-11-14 15:08:30 -05:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-11 10:30:21 -04:00
										 |  |  | 		tid = tid || utils.generateUUID(); | 
					
						
							|  |  |  | 		mainPost.tid = tid; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		const urlMap = chain.reduce((map, post) => (post.url ? map.set(post.url, post.id) : map), new Map()); | 
					
						
							|  |  |  | 		const unprocessed = chain.map((post) => { | 
					
						
							|  |  |  | 			post.tid = tid; // add tid to post hash
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			// Ensure toPids in replies are ids
 | 
					
						
							|  |  |  | 			if (urlMap.has(post.toPid)) { | 
					
						
							|  |  |  | 				post.toPid = urlMap.get(post.toPid); | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			return post; | 
					
						
							| 
									
										
										
										
											2025-09-22 12:14:14 -04:00
										 |  |  | 		}).filter((p, idx) => !exists[idx]); | 
					
						
							| 
									
										
										
										
											2025-09-11 10:30:21 -04:00
										 |  |  | 		const count = unprocessed.length; | 
					
						
							|  |  |  | 		activitypub.helpers.log(`[notes/assert] ${count} new note(s) found.`); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		if (!hasTid) { | 
					
						
							|  |  |  | 			const { to, cc, attachment } = mainPost._activitypub; | 
					
						
							|  |  |  | 			const tags = await Notes._normalizeTags(mainPost._activitypub.tag || []); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			try { | 
					
						
							|  |  |  | 				await topics.post({ | 
					
						
							|  |  |  | 					tid, | 
					
						
							|  |  |  | 					uid: authorId, | 
					
						
							|  |  |  | 					cid: options.cid || cid, | 
					
						
							|  |  |  | 					pid: mainPid, | 
					
						
							|  |  |  | 					title, | 
					
						
							|  |  |  | 					timestamp, | 
					
						
							|  |  |  | 					tags, | 
					
						
							|  |  |  | 					content: mainPost.content, | 
					
						
							|  |  |  | 					sourceContent: mainPost.sourceContent, | 
					
						
							|  |  |  | 					_activitypub: mainPost._activitypub, | 
					
						
							| 
									
										
										
										
											2024-11-14 15:08:30 -05:00
										 |  |  | 				}); | 
					
						
							| 
									
										
										
										
											2025-09-11 10:30:21 -04:00
										 |  |  | 				unprocessed.shift(); | 
					
						
							|  |  |  | 			} catch (e) { | 
					
						
							|  |  |  | 				activitypub.helpers.log(`[activitypub/notes.assert] Could not post topic (${mainPost.pid}): ${e.message}`); | 
					
						
							|  |  |  | 				return null; | 
					
						
							|  |  |  | 			} | 
					
						
							| 
									
										
										
										
											2024-02-26 11:39:32 -05:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-11 10:30:21 -04:00
										 |  |  | 			// These must come after topic is posted
 | 
					
						
							|  |  |  | 			await Promise.all([ | 
					
						
							|  |  |  | 				Notes.updateLocalRecipients(mainPid, { to, cc }), | 
					
						
							|  |  |  | 				mainPost._activitypub.image ? topics.thumbs.associate({ | 
					
						
							|  |  |  | 					id: tid, | 
					
						
							|  |  |  | 					path: mainPost._activitypub.image, | 
					
						
							|  |  |  | 				}) : null, | 
					
						
							|  |  |  | 				posts.attachments.update(mainPid, attachment), | 
					
						
							|  |  |  | 			]); | 
					
						
							| 
									
										
										
										
											2024-08-13 11:12:43 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-11 10:30:21 -04:00
										 |  |  | 			if (context) { | 
					
						
							|  |  |  | 				activitypub.helpers.log(`[activitypub/notes.assert] Associating tid ${tid} with context ${context}`); | 
					
						
							|  |  |  | 				await topics.setTopicField(tid, 'context', context); | 
					
						
							|  |  |  | 			} | 
					
						
							| 
									
										
										
										
											2024-08-13 11:12:43 -04:00
										 |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-11 10:30:21 -04:00
										 |  |  | 		for (const post of unprocessed) { | 
					
						
							|  |  |  | 			const { to, cc, attachment } = post._activitypub; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			try { | 
					
						
							|  |  |  | 				// eslint-disable-next-line no-await-in-loop
 | 
					
						
							|  |  |  | 				await topics.reply(post); | 
					
						
							|  |  |  | 				// eslint-disable-next-line no-await-in-loop
 | 
					
						
							|  |  |  | 				await Promise.all([ | 
					
						
							|  |  |  | 					Notes.updateLocalRecipients(post.pid, { to, cc }), | 
					
						
							|  |  |  | 					posts.attachments.update(post.pid, attachment), | 
					
						
							|  |  |  | 				]); | 
					
						
							|  |  |  | 			} catch (e) { | 
					
						
							|  |  |  | 				activitypub.helpers.log(`[activitypub/notes.assert] Could not add reply (${post.pid}): ${e.message}`); | 
					
						
							|  |  |  | 			} | 
					
						
							| 
									
										
										
										
											2025-06-01 00:31:58 -04:00
										 |  |  | 		} | 
					
						
							| 
									
										
										
										
											2024-10-24 14:35:34 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-19 10:34:57 -04:00
										 |  |  | 		await Notes.syncUserInboxes(tid, uid); | 
					
						
							| 
									
										
										
										
											2025-09-11 10:30:21 -04:00
										 |  |  | 		return { tid, count }; | 
					
						
							|  |  |  | 	} catch (e) { | 
					
						
							| 
									
										
										
										
											2025-09-19 10:34:57 -04:00
										 |  |  | 		winston.warn(`[activitypub/notes.assert] Could not assert ${id} (${e.message}).`); | 
					
						
							| 
									
										
										
										
											2025-09-11 10:30:21 -04:00
										 |  |  | 		return null; | 
					
						
							| 
									
										
										
										
											2025-09-19 10:34:57 -04:00
										 |  |  | 	} finally { | 
					
						
							|  |  |  | 		winston.verbose(`[activitypub/notes.assert] Releasing lock (${id})`); | 
					
						
							|  |  |  | 		await db.deleteObjectField('locks', id); | 
					
						
							| 
									
										
										
										
											2024-03-12 13:27:29 -04:00
										 |  |  | 	} | 
					
						
							| 
									
										
										
										
											2024-01-12 15:23:30 -05:00
										 |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-10-08 14:26:55 -04:00
										 |  |  | Notes.assertPrivate = async (object) => { | 
					
						
							|  |  |  | 	// Given an object, adds it to an existing chat or creates a new chat otherwise
 | 
					
						
							|  |  |  | 	// todo: context stuff
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-02-18 10:12:22 -05:00
										 |  |  | 	if (!object || !object.id || !activitypub.helpers.isUri(object.id)) { | 
					
						
							|  |  |  | 		return null; | 
					
						
							|  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2025-08-27 13:28:26 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-10-08 14:26:55 -04:00
										 |  |  | 	const localUids = []; | 
					
						
							| 
									
										
										
										
											2025-02-26 12:29:52 -05:00
										 |  |  | 	const recipients = new Set([...(object.to || []), ...(object.cc || [])]); | 
					
						
							| 
									
										
										
										
											2024-10-08 14:26:55 -04:00
										 |  |  | 	await Promise.all(Array.from(recipients).map(async (value) => { | 
					
						
							|  |  |  | 		const { type, id } = await activitypub.helpers.resolveLocalId(value); | 
					
						
							|  |  |  | 		if (type === 'user') { | 
					
						
							|  |  |  | 			localUids.push(id); | 
					
						
							| 
									
										
										
										
											2024-10-09 15:09:06 -04:00
										 |  |  | 			recipients.delete(value); | 
					
						
							|  |  |  | 			recipients.add(parseInt(id, 10)); | 
					
						
							| 
									
										
										
										
											2024-10-08 14:26:55 -04:00
										 |  |  | 		} | 
					
						
							|  |  |  | 	})); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-10-09 15:09:06 -04:00
										 |  |  | 	// Trim recipient list down to asserted actors (and local users) only
 | 
					
						
							|  |  |  | 	await activitypub.actors.assert([...recipients]); | 
					
						
							|  |  |  | 	const exists = await user.exists([...recipients]); | 
					
						
							|  |  |  | 	Array.from(recipients).forEach((uid, idx) => { | 
					
						
							|  |  |  | 		if (!exists[idx]) { | 
					
						
							|  |  |  | 			recipients.delete(uid); | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	}); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-10-08 14:26:55 -04:00
										 |  |  | 	// Locate the roomId based on `inReplyTo`
 | 
					
						
							|  |  |  | 	let roomId; | 
					
						
							|  |  |  | 	const resolved = await activitypub.helpers.resolveLocalId(object.inReplyTo); | 
					
						
							|  |  |  | 	let toMid = resolved.type === 'message' && resolved.id; | 
					
						
							|  |  |  | 	if (object.inReplyTo && await messaging.messageExists(toMid || object.inReplyTo)) { | 
					
						
							|  |  |  | 		roomId = await messaging.getMessageField(toMid || object.inReplyTo, 'roomId'); | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Compare room members with object recipients; if someone in-room is omitted, start new chat
 | 
					
						
							| 
									
										
										
										
											2024-10-08 16:28:42 -04:00
										 |  |  | 	const participants = await messaging.getUsersInRoom(roomId, 0, -1); | 
					
						
							|  |  |  | 	const participantUids = participants.map(user => user.uid); | 
					
						
							| 
									
										
										
										
											2024-10-08 14:26:55 -04:00
										 |  |  | 	if (roomId) { | 
					
						
							|  |  |  | 		const omitted = participants.filter((user) => { | 
					
						
							| 
									
										
										
										
											2024-10-09 15:09:06 -04:00
										 |  |  | 			const { uid } = user; | 
					
						
							| 
									
										
										
										
											2024-10-08 14:26:55 -04:00
										 |  |  | 			return !recipients.has(uid) && uid !== object.attributedTo; | 
					
						
							|  |  |  | 		}); | 
					
						
							|  |  |  | 		if (omitted.length) { | 
					
						
							|  |  |  | 			toMid = undefined; // message creation logic fails if toMid is not in room
 | 
					
						
							|  |  |  | 			roomId = null; | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	let timestamp; | 
					
						
							|  |  |  | 	try { | 
					
						
							|  |  |  | 		timestamp = new Date(object.published).getTime() || Date.now(); | 
					
						
							|  |  |  | 	} catch (e) { | 
					
						
							|  |  |  | 		timestamp = Date.now(); | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-04 13:54:36 -05:00
										 |  |  | 	const payload = await activitypub.mocks.message(object); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-07 15:26:00 -05:00
										 |  |  | 	try { | 
					
						
							|  |  |  | 		await messaging.checkContent(payload.content, false); | 
					
						
							|  |  |  | 	} catch (e) { | 
					
						
							|  |  |  | 		const { displayname, userslug } = await user.getUserFields(payload.uid, ['displayname', 'userslug']); | 
					
						
							|  |  |  | 		const notification = await notifications.create({ | 
					
						
							|  |  |  | 			bodyShort: `[[error:remote-chat-received-too-long, ${displayname}]]`, | 
					
						
							|  |  |  | 			path: `/user/${userslug}`, | 
					
						
							|  |  |  | 			nid: `error:chat:uid:${payload.uid}`, | 
					
						
							|  |  |  | 			from: payload.uid, | 
					
						
							|  |  |  | 		}); | 
					
						
							|  |  |  | 		notifications.push(notification, Array.from(recipients).filter(uid => utils.isNumber(uid))); | 
					
						
							|  |  |  | 		return null; | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-10-08 14:26:55 -04:00
										 |  |  | 	if (!roomId) { | 
					
						
							| 
									
										
										
										
											2025-03-04 13:54:36 -05:00
										 |  |  | 		roomId = await messaging.newRoom(payload.uid, { uids: [...recipients] }); | 
					
						
							| 
									
										
										
										
											2024-10-08 14:26:55 -04:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-10-08 16:28:42 -04:00
										 |  |  | 	// Add any new members to the chat
 | 
					
						
							| 
									
										
										
										
											2024-10-09 15:09:06 -04:00
										 |  |  | 	const added = Array.from(recipients).filter(uid => !participantUids.includes(uid)); | 
					
						
							| 
									
										
										
										
											2024-10-08 16:28:42 -04:00
										 |  |  | 	const assertion = await activitypub.actors.assert(added); | 
					
						
							|  |  |  | 	if (assertion) { | 
					
						
							| 
									
										
										
										
											2025-03-04 13:54:36 -05:00
										 |  |  | 		await messaging.addUsersToRoom(payload.uid, added, roomId); | 
					
						
							| 
									
										
										
										
											2024-10-08 16:28:42 -04:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-10-08 14:26:55 -04:00
										 |  |  | 	// Add message to room
 | 
					
						
							| 
									
										
										
										
											2024-10-09 13:46:51 -04:00
										 |  |  | 	const message = await messaging.sendMessage({ | 
					
						
							| 
									
										
										
										
											2025-03-04 13:54:36 -05:00
										 |  |  | 		...payload, | 
					
						
							|  |  |  | 		timestamp: Date.now(), | 
					
						
							| 
									
										
										
										
											2024-10-08 14:26:55 -04:00
										 |  |  | 		roomId: roomId, | 
					
						
							|  |  |  | 		toMid: toMid, | 
					
						
							|  |  |  | 	}); | 
					
						
							| 
									
										
										
										
											2025-03-04 13:54:36 -05:00
										 |  |  | 	messaging.notifyUsersInRoom(payload.uid, roomId, message); | 
					
						
							| 
									
										
										
										
											2024-10-08 14:26:55 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-10-08 23:51:41 -04:00
										 |  |  | 	// Set real timestamp back so that the message shows even though it predates room joining
 | 
					
						
							| 
									
										
										
										
											2025-03-04 13:54:36 -05:00
										 |  |  | 	await messaging.setMessageField(payload.mid, 'timestamp', timestamp); | 
					
						
							| 
									
										
										
										
											2024-10-08 23:51:41 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-10-08 14:26:55 -04:00
										 |  |  | 	return { roomId }; | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											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-07-19 14:37:32 -04:00
										 |  |  | 	const { followers } = await activitypub.actors.getLocalFollowCounts(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-07-19 14:37:32 -04:00
										 |  |  | 	return followers > 0 || uids.length; | 
					
						
							| 
									
										
										
										
											2024-03-26 10:22:17 -04:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-25 11:47:01 -04:00
										 |  |  | async function assignCategory(post) { | 
					
						
							| 
									
										
										
										
											2025-09-08 12:00:32 -04:00
										 |  |  | 	activitypub.helpers.log('[activitypub] Checking auto-categorization rules.'); | 
					
						
							| 
									
										
										
										
											2025-08-25 11:47:01 -04:00
										 |  |  | 	let cid = undefined; | 
					
						
							|  |  |  | 	const rules = await activitypub.rules.list(); | 
					
						
							| 
									
										
										
										
											2025-09-08 12:00:32 -04:00
										 |  |  | 	let tags = await Notes._normalizeTags(post._activitypub.tag || []); | 
					
						
							|  |  |  | 	tags = tags.map(tag => tag.toLowerCase()); | 
					
						
							| 
									
										
										
										
											2025-08-25 11:47:01 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	cid = rules.reduce((cid, { type, value, cid: target }) => { | 
					
						
							|  |  |  | 		if (!cid) { | 
					
						
							|  |  |  | 			switch (type) { | 
					
						
							|  |  |  | 				case 'hashtag': { | 
					
						
							| 
									
										
										
										
											2025-09-08 12:00:32 -04:00
										 |  |  | 					if (tags.includes(value.toLowerCase())) { | 
					
						
							|  |  |  | 						activitypub.helpers.log(`[activitypub]   - Rule match: #${value}; cid: ${target}`); | 
					
						
							| 
									
										
										
										
											2025-08-25 11:47:01 -04:00
										 |  |  | 						return target; | 
					
						
							|  |  |  | 					} | 
					
						
							|  |  |  | 					break; | 
					
						
							|  |  |  | 				} | 
					
						
							| 
									
										
										
										
											2025-09-08 14:57:51 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  | 				case 'user': { | 
					
						
							|  |  |  | 					if (post.uid === value) { | 
					
						
							|  |  |  | 						activitypub.helpers.log(`[activitypub]   - Rule match: user ${value}; cid: ${target}`); | 
					
						
							|  |  |  | 						return target; | 
					
						
							|  |  |  | 					} | 
					
						
							|  |  |  | 				} | 
					
						
							| 
									
										
										
										
											2025-08-25 11:47:01 -04:00
										 |  |  | 			} | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		return cid; | 
					
						
							|  |  |  | 	}, cid); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return cid; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											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) { | 
					
						
							| 
									
										
										
										
											2025-02-14 20:43:19 -05:00
										 |  |  | 	const [pids, { cid, mainPid, tags }] = await Promise.all([ | 
					
						
							| 
									
										
										
										
											2024-02-12 14:51:21 -05:00
										 |  |  | 		db.getSortedSetMembers(`tid:${tid}:posts`), | 
					
						
							| 
									
										
										
										
											2025-02-14 20:43:19 -05:00
										 |  |  | 		topics.getTopicFields(tid, ['tid', 'cid', 'mainPid', 'tags']), | 
					
						
							| 
									
										
										
										
											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)); | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-02-14 20:43:19 -05:00
										 |  |  | 	// Tag followers
 | 
					
						
							|  |  |  | 	const tagsFollowers = await topics.getTagsFollowers(tags.map(tag => tag.value)); | 
					
						
							|  |  |  | 	new Set(tagsFollowers.flat()).forEach((uid) => { | 
					
						
							|  |  |  | 		uids.add(uid); | 
					
						
							|  |  |  | 	}); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-20 13:02:30 -04:00
										 |  |  | 	// Category followers
 | 
					
						
							|  |  |  | 	const categoryFollowers = await activitypub.actors.getLocalFollowers(cid); | 
					
						
							|  |  |  | 	categoryFollowers.uids.forEach((uid) => { | 
					
						
							|  |  |  | 		uids.add(uid); | 
					
						
							|  |  |  | 	}); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											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-10-12 22:49:24 -04:00
										 |  |  | 	activitypub.helpers.log(`[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()) => { | 
					
						
							| 
									
										
										
										
											2025-04-01 10:55:03 -04:00
										 |  |  | 	const [tid] = await Promise.all([ | 
					
						
							|  |  |  | 		posts.getPostField(pid, 'tid'), | 
					
						
							| 
									
										
										
										
											2024-12-16 13:50:24 -05:00
										 |  |  | 		db.sortedSetAdd(`pid:${pid}:announces`, timestamp, actor), | 
					
						
							| 
									
										
										
										
											2025-04-01 10:55:03 -04:00
										 |  |  | 	]); | 
					
						
							|  |  |  | 	await Promise.all([ | 
					
						
							| 
									
										
										
										
											2024-12-16 13:50:24 -05:00
										 |  |  | 		posts.setPostField(pid, 'announces', await db.sortedSetCard(`pid:${pid}:announces`)), | 
					
						
							|  |  |  | 		topics.tools.share(tid, actor, timestamp), | 
					
						
							|  |  |  | 	]); | 
					
						
							| 
									
										
										
										
											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'; | 
					
						
							| 
									
										
										
										
											2025-03-19 10:17:09 -04:00
										 |  |  | 	const stop = Date.now() - (1000 * 60 * 60 * 24 * meta.config.activitypubContentPruneDays); | 
					
						
							| 
									
										
										
										
											2024-06-04 14:18:14 -04:00
										 |  |  | 	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
										 |  |  | }; |