mirror of
				https://github.com/NodeBB/NodeBB.git
				synced 2025-10-31 11:05:54 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			627 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			627 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| 'use strict';
 | |
| 
 | |
| const winston = require('winston');
 | |
| const nconf = require('nconf');
 | |
| 
 | |
| const db = require('../database');
 | |
| const privileges = require('../privileges');
 | |
| const user = require('../user');
 | |
| const posts = require('../posts');
 | |
| const topics = require('../topics');
 | |
| const categories = require('../categories');
 | |
| const notifications = require('../notifications');
 | |
| const messaging = require('../messaging');
 | |
| const flags = require('../flags');
 | |
| const api = require('../api');
 | |
| const activitypub = require('.');
 | |
| 
 | |
| const socketHelpers = require('../socket.io/helpers');
 | |
| const helpers = require('./helpers');
 | |
| 
 | |
| const inbox = module.exports;
 | |
| 
 | |
| function reject(type, object, target, senderType = 'uid', id = 0) {
 | |
| 	activitypub.send(senderType, id, target, {
 | |
| 		id: `${helpers.resolveActor(senderType, id)}#/activity/reject/${encodeURIComponent(object.id)}`,
 | |
| 		type: 'Reject',
 | |
| 		object: {
 | |
| 			type,
 | |
| 			target,
 | |
| 			object,
 | |
| 		},
 | |
| 	}).catch(err => winston.error(err.stack));
 | |
| }
 | |
| 
 | |
| inbox.create = async (req) => {
 | |
| 	const { object, actor } = req.body;
 | |
| 	const start = Date.now();
 | |
| 	// Alternative logic for non-public objects
 | |
| 	const isPublic = [...(object.to || []), ...(object.cc || [])].includes(activitypub._constants.publicAddress);
 | |
| 	if (!isPublic) {
 | |
| 		return await activitypub.notes.assertPrivate(object);
 | |
| 	}
 | |
| 	console.log(' 4a', Date.now() - start);
 | |
| 	// Category sync, remove when cross-posting available
 | |
| 	const { cids } = await activitypub.actors.getLocalFollowers(actor);
 | |
| 	let cid = null;
 | |
| 	if (cids.size > 0) {
 | |
| 		cid = Array.from(cids)[0];
 | |
| 	}
 | |
| 	console.log(' 4b', Date.now() - start);
 | |
| 	const asserted = await activitypub.notes.assert(0, object, { cid });
 | |
| 	console.log(' 4c', Date.now() - start);
 | |
| 	if (asserted) {
 | |
| 		await activitypub.feps.announce(object.id, req.body);
 | |
| 		// api.activitypub.add(req, { pid: object.id });
 | |
| 	}
 | |
| 	console.log(' 4d', Date.now() - start);
 | |
| };
 | |
| 
 | |
| inbox.add = async (req) => {
 | |
| 	const { actor, object, target } = req.body;
 | |
| 
 | |
| 	// Only react on Adds pertaining to local posts
 | |
| 	const { type, id: pid } = await activitypub.helpers.resolveLocalId(object);
 | |
| 	if (type === 'post') {
 | |
| 		// Check context of OP
 | |
| 		const tid = await posts.getPostField(pid, 'tid');
 | |
| 		const context = await topics.getTopicField(tid, 'context');
 | |
| 		if (context) {
 | |
| 			const { attributedTo } = await activitypub.get('uid', 0, context);
 | |
| 			if (context === target && attributedTo === actor) {
 | |
| 				activitypub.helpers.log(`[activitypub/inbox/add] Associating pid ${pid} with new context ${target}`);
 | |
| 				await posts.setPostField(pid, 'context', target);
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| };
 | |
| 
 | |
| inbox.update = async (req) => {
 | |
| 	const { actor, object } = req.body;
 | |
| 	const isPublic = [...(object.to || []), ...(object.cc || [])].includes(activitypub._constants.publicAddress);
 | |
| 
 | |
| 	// Origin checking
 | |
| 	const actorHostname = new URL(actor).hostname;
 | |
| 	const objectHostname = new URL(object.id).hostname;
 | |
| 	if (actorHostname !== objectHostname) {
 | |
| 		throw new Error('[[error:activitypub.origin-mismatch]]');
 | |
| 	}
 | |
| 
 | |
| 	switch (true) {
 | |
| 		case activitypub._constants.acceptedPostTypes.includes(object.type): {
 | |
| 			const [isNote, isMessage] = await Promise.all([
 | |
| 				posts.exists(object.id),
 | |
| 				messaging.messageExists(object.id),
 | |
| 			]);
 | |
| 
 | |
| 			try {
 | |
| 				switch (true) {
 | |
| 					case isNote: {
 | |
| 						const cid = await posts.getCidByPid(object.id);
 | |
| 						const allowed = await privileges.categories.can('posts:edit', cid, activitypub._constants.uid);
 | |
| 						if (!allowed) {
 | |
| 							throw new Error('[[error:no-privileges]]');
 | |
| 						}
 | |
| 
 | |
| 						const postData = await activitypub.mocks.post(object);
 | |
| 						postData.tags = await activitypub.notes._normalizeTags(postData._activitypub.tag, postData.cid);
 | |
| 						await posts.edit(postData);
 | |
| 						const isDeleted = await posts.getPostField(object.id, 'deleted');
 | |
| 						if (isDeleted) {
 | |
| 							await api.posts.restore({ uid: actor }, { pid: object.id });
 | |
| 						}
 | |
| 						break;
 | |
| 					}
 | |
| 
 | |
| 					case isMessage: {
 | |
| 						const { roomId, deleted } = await messaging.getMessageFields(object.id, ['roomId', 'deleted']);
 | |
| 						await messaging.editMessage(actor, object.id, roomId, object.content);
 | |
| 						if (deleted) {
 | |
| 							await api.chats.restoreMessage({ uid: actor }, { mid: object.id });
 | |
| 						}
 | |
| 						break;
 | |
| 					}
 | |
| 
 | |
| 					default: {
 | |
| 						if (!isPublic) {
 | |
| 							return await activitypub.notes.assertPrivate(object);
 | |
| 						}
 | |
| 
 | |
| 						const { cids } = await activitypub.actors.getLocalFollowers(actor);
 | |
| 						let cid = null;
 | |
| 						if (cids.size > 0) {
 | |
| 							cid = Array.from(cids)[0];
 | |
| 						}
 | |
| 
 | |
| 						const asserted = await activitypub.notes.assert(0, object.id, { cid });
 | |
| 						if (asserted) {
 | |
| 							activitypub.feps.announce(object.id, req.body);
 | |
| 						}
 | |
| 						break;
 | |
| 					}
 | |
| 				}
 | |
| 			} catch (e) {
 | |
| 				reject('Update', object, actor);
 | |
| 			}
 | |
| 			break;
 | |
| 		}
 | |
| 
 | |
| 		case activitypub._constants.acceptableActorTypes.has(object.type): {
 | |
| 			await activitypub.actors.assert(object.id, { update: true });
 | |
| 			break;
 | |
| 		}
 | |
| 
 | |
| 		case object.type === 'Tombstone': {
 | |
| 			const [isNote, isMessage/* , isActor */] = await Promise.all([
 | |
| 				posts.exists(object.id),
 | |
| 				messaging.messageExists(object.id),
 | |
| 				// db.isSortedSetMember('usersRemote:lastCrawled', object.id),
 | |
| 			]);
 | |
| 
 | |
| 			switch (true) {
 | |
| 				case isNote: {
 | |
| 					await api.posts.delete({ uid: actor }, { pid: object.id });
 | |
| 					break;
 | |
| 				}
 | |
| 
 | |
| 				case isMessage: {
 | |
| 					await api.chats.deleteMessage({ uid: actor }, { mid: object.id });
 | |
| 					break;
 | |
| 				}
 | |
| 
 | |
| 				// case isActor: {
 | |
| 				// console.log('actor');
 | |
| 				// break;
 | |
| 				// }
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| };
 | |
| 
 | |
| inbox.delete = async (req) => {
 | |
| 	const { actor, object } = req.body;
 | |
| 	if (typeof object !== 'string') {
 | |
| 		const { id } = object;
 | |
| 		if (!id) {
 | |
| 			throw new Error('[[error:invalid-pid]]');
 | |
| 		}
 | |
| 	}
 | |
| 	const pid = object.id || object;
 | |
| 	let type = object.type || undefined;
 | |
| 
 | |
| 	// Deletes don't have their objects resolved automatically
 | |
| 	let method = 'purge';
 | |
| 	try {
 | |
| 		if (!type) {
 | |
| 			({ type } = await activitypub.get('uid', 0, pid));
 | |
| 		}
 | |
| 
 | |
| 		if (type === 'Tombstone') {
 | |
| 			method = 'delete';
 | |
| 		}
 | |
| 	} catch (e) {
 | |
| 		// probably 410/404
 | |
| 	}
 | |
| 
 | |
| 	// Deletions must be made by an actor of the same origin
 | |
| 	const actorHostname = new URL(actor).hostname;
 | |
| 
 | |
| 	const objectHostname = new URL(pid).hostname;
 | |
| 	if (actorHostname !== objectHostname) {
 | |
| 		return reject('Delete', object, actor);
 | |
| 	}
 | |
| 
 | |
| 	const [isNote/* , isActor */] = await Promise.all([
 | |
| 		posts.exists(pid),
 | |
| 		// db.isSortedSetMember('usersRemote:lastCrawled', object.id),
 | |
| 	]);
 | |
| 
 | |
| 	switch (true) {
 | |
| 		case isNote: {
 | |
| 			const cid = await posts.getCidByPid(pid);
 | |
| 			const allowed = await privileges.categories.can('posts:edit', cid, activitypub._constants.uid);
 | |
| 			if (!allowed) {
 | |
| 				return reject('Delete', object, actor);
 | |
| 			}
 | |
| 
 | |
| 			const uid = await posts.getPostField(pid, 'uid');
 | |
| 			await activitypub.feps.announce(pid, req.body);
 | |
| 			await api.posts[method]({ uid }, { pid });
 | |
| 			break;
 | |
| 		}
 | |
| 
 | |
| 		// case isActor: {
 | |
| 		// console.log('actor');
 | |
| 		// break;
 | |
| 		// }
 | |
| 
 | |
| 		default: {
 | |
| 			activitypub.helpers.log(`[activitypub/inbox.delete] Object (${pid}) does not exist locally. Doing nothing.`);
 | |
| 			break;
 | |
| 		}
 | |
| 	}
 | |
| };
 | |
| 
 | |
| inbox.like = async (req) => {
 | |
| 	const { actor, object } = req.body;
 | |
| 	const { type, id } = await activitypub.helpers.resolveLocalId(object.id);
 | |
| 
 | |
| 	if (type !== 'post' || !(await posts.exists(id))) {
 | |
| 		return reject('Like', object, actor);
 | |
| 	}
 | |
| 
 | |
| 	const allowed = await privileges.posts.can('posts:upvote', id, activitypub._constants.uid);
 | |
| 	if (!allowed) {
 | |
| 		activitypub.helpers.log(`[activitypub/inbox.like] ${id} not allowed to be upvoted.`);
 | |
| 		return reject('Like', object, actor);
 | |
| 	}
 | |
| 
 | |
| 	activitypub.helpers.log(`[activitypub/inbox/like] id ${id} via ${actor}`);
 | |
| 
 | |
| 	const result = await posts.upvote(id, actor);
 | |
| 	await activitypub.feps.announce(object.id, req.body);
 | |
| 	socketHelpers.upvote(result, 'notifications:upvoted-your-post-in');
 | |
| };
 | |
| 
 | |
| inbox.announce = async (req) => {
 | |
| 	let { actor, object, published, to, cc } = req.body;
 | |
| 	activitypub.helpers.log(`[activitypub/inbox/announce] Parsing Announce(${object.type}) from ${actor}`);
 | |
| 	let timestamp = new Date(published);
 | |
| 	timestamp = timestamp.toString() !== 'Invalid Date' ? timestamp.getTime() : Date.now();
 | |
| 
 | |
| 	const assertion = await activitypub.actors.assert(actor);
 | |
| 	if (!assertion) {
 | |
| 		throw new Error('[[error:activitypub.invalid-id]]');
 | |
| 	}
 | |
| 
 | |
| 	let tid;
 | |
| 	let pid;
 | |
| 
 | |
| 	// Category sync, remove when cross-posting available
 | |
| 	const { cids } = await activitypub.actors.getLocalFollowers(actor);
 | |
| 	let cid = null;
 | |
| 	if (cids.size > 0) {
 | |
| 		cid = Array.from(cids)[0];
 | |
| 	}
 | |
| 
 | |
| 	// 1b12 announce
 | |
| 	const categoryActor = await categories.exists(actor);
 | |
| 	if (categoryActor) {
 | |
| 		cid = actor;
 | |
| 	}
 | |
| 
 | |
| 	switch(true) {
 | |
| 		case object.type === 'Like': {
 | |
| 			const id = object.object.id || object.object;
 | |
| 			const { id: localId } = await activitypub.helpers.resolveLocalId(id);
 | |
| 			const exists = await posts.exists(localId || id);
 | |
| 			if (exists) {
 | |
| 				try {
 | |
| 					const result = await posts.upvote(localId || id, object.actor);
 | |
| 					if (localId) {
 | |
| 						socketHelpers.upvote(result, 'notifications:upvoted-your-post-in');
 | |
| 					}
 | |
| 				} catch (e) {
 | |
| 					// vote denied due to local limitations (frequency, privilege, etc.); noop.
 | |
| 				}
 | |
| 			}
 | |
| 
 | |
| 			break;
 | |
| 		}
 | |
| 
 | |
| 		case object.type === 'Update': {
 | |
| 			req.body = object;
 | |
| 			await inbox.update(req);
 | |
| 			break;
 | |
| 		}
 | |
| 
 | |
| 		case object.type === 'Create': {
 | |
| 			object = object.object;
 | |
| 			// falls through
 | |
| 		}
 | |
| 
 | |
| 		// Announce(Object)
 | |
| 		case activitypub._constants.acceptedPostTypes.includes(object.type): {
 | |
| 			if (String(object.id).startsWith(nconf.get('url'))) { // Local object
 | |
| 				const { type, id } = await activitypub.helpers.resolveLocalId(object.id);
 | |
| 				if (type !== 'post' || !(await posts.exists(id))) {
 | |
| 					reject('Announce', object, actor);
 | |
| 					return;
 | |
| 				}
 | |
| 
 | |
| 				pid = id;
 | |
| 				tid = await posts.getPostField(id, 'tid');
 | |
| 
 | |
| 				socketHelpers.sendNotificationToPostOwner(pid, actor, 'announce', 'notifications:activitypub.announce');
 | |
| 			} else { // Remote object
 | |
| 				// Follower check
 | |
| 				if (!cid) {
 | |
| 					const { followers } = await activitypub.actors.getLocalFollowCounts(actor);
 | |
| 					if (!followers) {
 | |
| 						winston.verbose(`[activitypub/inbox.announce] Rejecting ${object.id} via ${actor} due to no followers`);
 | |
| 						reject('Announce', object, actor);
 | |
| 						return;
 | |
| 					}
 | |
| 				}
 | |
| 
 | |
| 				pid = object.id;
 | |
| 				pid = await activitypub.resolveId(0, pid); // in case wrong id is passed-in; unlikely, but still.
 | |
| 				if (!pid) {
 | |
| 					return;
 | |
| 				}
 | |
| 
 | |
| 				const assertion = await activitypub.notes.assert(0, pid, { cid, skipChecks: true });
 | |
| 				if (!assertion) {
 | |
| 					return;
 | |
| 				}
 | |
| 
 | |
| 				({ tid } = assertion);
 | |
| 				await activitypub.notes.updateLocalRecipients(pid, { to, cc });
 | |
| 				await activitypub.notes.syncUserInboxes(tid);
 | |
| 			}
 | |
| 
 | |
| 			if (!cid) { // Topic events from actors followed by users only
 | |
| 				await activitypub.notes.announce.add(pid, actor, timestamp);
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| };
 | |
| 
 | |
| inbox.follow = async (req) => {
 | |
| 	const { actor, object, id: followId } = req.body;
 | |
| 	// Sanity checks
 | |
| 	const { type, id } = await helpers.resolveLocalId(object.id);
 | |
| 	if (!['category', 'user'].includes(type)) {
 | |
| 		throw new Error('[[error:activitypub.invalid-id]]');
 | |
| 	}
 | |
| 
 | |
| 	const assertion = await activitypub.actors.assert(actor);
 | |
| 	if (!assertion) {
 | |
| 		throw new Error('[[error:activitypub.invalid-id]]');
 | |
| 	}
 | |
| 	const handle = await user.getUserField(actor, 'username');
 | |
| 
 | |
| 	if (type === 'user') {
 | |
| 		const [exists, allowed] = await Promise.all([
 | |
| 			user.exists(id),
 | |
| 			privileges.global.can('view:users', activitypub._constants.uid),
 | |
| 		]);
 | |
| 		if (!exists || !allowed) {
 | |
| 			throw new Error('[[error:invalid-uid]]');
 | |
| 		}
 | |
| 
 | |
| 		const isFollowed = await inbox.isFollowed(actor, id);
 | |
| 		if (isFollowed) {
 | |
| 			// No additional parsing required
 | |
| 			return;
 | |
| 		}
 | |
| 
 | |
| 		const now = Date.now();
 | |
| 		await db.sortedSetAdd(`followersRemote:${id}`, now, actor);
 | |
| 		await db.sortedSetAdd(`followingRemote:${actor}`, now, id); // for following backreference (actor pruning)
 | |
| 
 | |
| 		const followerRemoteCount = await db.sortedSetCard(`followersRemote:${id}`);
 | |
| 		await user.setUserField(id, 'followerRemoteCount', followerRemoteCount);
 | |
| 
 | |
| 		await user.onFollow(actor, id);
 | |
| 		activitypub.send('uid', id, actor, {
 | |
| 			id: `${nconf.get('url')}/${type}/${id}#activity/accept:follow/${handle}/${Date.now()}`,
 | |
| 			type: 'Accept',
 | |
| 			object: {
 | |
| 				id: followId,
 | |
| 				type: 'Follow',
 | |
| 				actor,
 | |
| 				object: object.id,
 | |
| 			},
 | |
| 		}).catch(err => winston.error(err.stack));
 | |
| 	} else if (type === 'category') {
 | |
| 		const [exists, allowed] = await Promise.all([
 | |
| 			categories.exists(id),
 | |
| 			privileges.categories.can('read', id, activitypub._constants.uid),
 | |
| 		]);
 | |
| 		if (!exists) {
 | |
| 			throw new Error('[[error:invalid-cid]]');
 | |
| 		}
 | |
| 		if (!allowed) {
 | |
| 			return reject('Follow', object, actor);
 | |
| 		}
 | |
| 
 | |
| 		const watchState = await categories.getWatchState([id], actor);
 | |
| 		if (watchState[0] !== categories.watchStates.tracking) {
 | |
| 			await user.setCategoryWatchState(actor, id, categories.watchStates.tracking);
 | |
| 		}
 | |
| 
 | |
| 		activitypub.send('cid', id, actor, {
 | |
| 			id: `${nconf.get('url')}/${type}/${id}#activity/accept:follow/${handle}/${Date.now()}`,
 | |
| 			type: 'Accept',
 | |
| 			object: {
 | |
| 				id: followId,
 | |
| 				type: 'Follow',
 | |
| 				actor,
 | |
| 				object: object.id,
 | |
| 			},
 | |
| 		}).catch(err => winston.error(err.stack));
 | |
| 	}
 | |
| };
 | |
| 
 | |
| inbox.isFollowed = async (actorId, uid) => {
 | |
| 	if (actorId.indexOf('@') === -1 || parseInt(uid, 10) <= 0) {
 | |
| 		return false;
 | |
| 	}
 | |
| 	return await db.isSortedSetMember(`followersRemote:${uid}`, actorId);
 | |
| };
 | |
| 
 | |
| inbox.accept = async (req) => {
 | |
| 	const { actor, object } = req.body;
 | |
| 	const { type } = object;
 | |
| 
 | |
| 	const { type: localType, id } = await helpers.resolveLocalId(object.actor);
 | |
| 	if (!['user', 'category'].includes(localType)) {
 | |
| 		throw new Error('[[error:invalid-data]]');
 | |
| 	}
 | |
| 
 | |
| 	const assertion = await activitypub.actors.assert(actor);
 | |
| 	if (!assertion) {
 | |
| 		throw new Error('[[error:activitypub.invalid-id]]');
 | |
| 	}
 | |
| 
 | |
| 	if (type === 'Follow') {
 | |
| 		if (localType === 'user') {
 | |
| 			if (!await db.isSortedSetMember(`followRequests:uid.${id}`, actor)) {
 | |
| 				if (await db.isSortedSetMember(`followingRemote:${id}`, actor)) return; // already following
 | |
| 				return reject('Accept', req.body, actor); // not following, not requested, so reject to hopefully stop retries
 | |
| 			}
 | |
| 			const timestamp = await db.sortedSetScore(`followRequests:uid.${id}`, actor);
 | |
| 			await Promise.all([
 | |
| 				db.sortedSetRemove(`followRequests:uid.${id}`, actor),
 | |
| 				db.sortedSetAdd(`followingRemote:${id}`, timestamp, actor),
 | |
| 				db.sortedSetAdd(`followersRemote:${actor}`, timestamp, id), // for followers backreference and notes assertion checking
 | |
| 			]);
 | |
| 			const followingRemoteCount = await db.sortedSetCard(`followingRemote:${id}`);
 | |
| 			await user.setUserField(id, 'followingRemoteCount', followingRemoteCount);
 | |
| 		} else if (localType === 'category') {
 | |
| 			if (!await db.isSortedSetMember(`followRequests:cid.${id}`, actor)) {
 | |
| 				if (await db.isSortedSetMember(`cid:${id}:following`, actor)) return; // already following
 | |
| 				return reject('Accept', req.body, actor); // not following, not requested, so reject to hopefully stop retries
 | |
| 			}
 | |
| 			const timestamp = await db.sortedSetScore(`followRequests:cid.${id}`, actor);
 | |
| 			await Promise.all([
 | |
| 				db.sortedSetRemove(`followRequests:cid.${id}`, actor),
 | |
| 				db.sortedSetAdd(`cid:${id}:following`, timestamp, actor),
 | |
| 				db.sortedSetAdd(`followersRemote:${actor}`, timestamp, `cid|${id}`), // for notes assertion checking
 | |
| 			]);
 | |
| 		}
 | |
| 	}
 | |
| };
 | |
| 
 | |
| inbox.undo = async (req) => {
 | |
| 	// todo: "actor" in this case should be the one in object, no?
 | |
| 	const { actor, object } = req.body;
 | |
| 	const { type } = object;
 | |
| 
 | |
| 	if (actor !== object.actor) {
 | |
| 		throw new Error('[[error:activitypub.actor-mismatch]]');
 | |
| 	}
 | |
| 
 | |
| 	const assertion = await activitypub.actors.assert(actor);
 | |
| 	if (!assertion) {
 | |
| 		throw new Error('[[error:activitypub.invalid-id]]');
 | |
| 	}
 | |
| 
 | |
| 	let { type: localType, id } = await helpers.resolveLocalId(object.object);
 | |
| 
 | |
| 	winston.verbose(`[activitypub/inbox/undo] ${type} ${localType && id ? `${localType} ${id}` : object.object} via ${actor}`);
 | |
| 
 | |
| 	switch (type) {
 | |
| 		case 'Follow': {
 | |
| 			switch (localType) {
 | |
| 				case 'user': {
 | |
| 					const exists = await user.exists(id);
 | |
| 					if (!exists) {
 | |
| 						throw new Error('[[error:invalid-uid]]');
 | |
| 					}
 | |
| 
 | |
| 					await Promise.all([
 | |
| 						db.sortedSetRemove(`followersRemote:${id}`, actor),
 | |
| 						db.sortedSetRemove(`followingRemote:${actor}`, id),
 | |
| 					]);
 | |
| 					const followerRemoteCount = await db.sortedSetCard(`followerRemote:${id}`);
 | |
| 					await user.setUserField(id, 'followerRemoteCount', followerRemoteCount);
 | |
| 					notifications.rescind(`follow:${id}:uid:${actor}`);
 | |
| 					break;
 | |
| 				}
 | |
| 
 | |
| 				case 'category': {
 | |
| 					const exists = await categories.exists(id);
 | |
| 					if (!exists) {
 | |
| 						throw new Error('[[error:invalid-cid]]');
 | |
| 					}
 | |
| 
 | |
| 					await user.setCategoryWatchState(actor, id, categories.watchStates.notwatching);
 | |
| 					break;
 | |
| 				}
 | |
| 			}
 | |
| 
 | |
| 			break;
 | |
| 		}
 | |
| 
 | |
| 		case 'Like': {
 | |
| 			const exists = await posts.exists(id);
 | |
| 			if (localType !== 'post' || !exists) {
 | |
| 				reject('Like', object, actor);
 | |
| 				break;
 | |
| 			}
 | |
| 
 | |
| 			const allowed = await privileges.posts.can('posts:upvote', id, activitypub._constants.uid);
 | |
| 			if (!allowed) {
 | |
| 				winston.verbose(`[activitypub/inbox.like] ${id} not allowed to be upvoted.`);
 | |
| 				reject('Like', object, actor);
 | |
| 				break;
 | |
| 			}
 | |
| 
 | |
| 			await posts.unvote(id, actor);
 | |
| 			activitypub.feps.announce(object.object, req.body);
 | |
| 			notifications.rescind(`upvote:post:${id}:uid:${actor}`);
 | |
| 			break;
 | |
| 		}
 | |
| 
 | |
| 		case 'Announce': {
 | |
| 			id = id || object.object; // remote announces
 | |
| 			const exists = await posts.exists(id);
 | |
| 			if (!exists) {
 | |
| 				activitypub.helpers.log(`[activitypub/inbox/undo] Attempted to undo announce of ${id} but couldn't find it, so doing nothing.`);
 | |
| 				break;
 | |
| 			}
 | |
| 
 | |
| 			await activitypub.notes.announce.remove(id, actor);
 | |
| 			notifications.rescind(`announce:post:${id}:uid:${actor}`);
 | |
| 			break;
 | |
| 		}
 | |
| 		case 'Flag': {
 | |
| 			if (!Array.isArray(object.object)) {
 | |
| 				object.object = [object.object];
 | |
| 			}
 | |
| 			await Promise.all(object.object.map(async (subject) => {
 | |
| 				const { type, id } = await activitypub.helpers.resolveLocalId(subject.id);
 | |
| 				try {
 | |
| 					await flags.rescindReport(type, id, actor);
 | |
| 				} catch (e) {
 | |
| 					reject('Undo', { type: 'Flag', object: [subject] }, actor);
 | |
| 				}
 | |
| 			}));
 | |
| 			break;
 | |
| 		}
 | |
| 	}
 | |
| };
 | |
| inbox.flag = async (req) => {
 | |
| 	const { actor, object, content } = req.body;
 | |
| 	const objects = Array.isArray(object) ? object : [object];
 | |
| 
 | |
| 	// Check if the actor is valid
 | |
| 	if (!await activitypub.actors.assert(actor)) {
 | |
| 		return reject('Flag', objects, actor);
 | |
| 	}
 | |
| 
 | |
| 	await Promise.all(objects.map(async (subject, index) => {
 | |
| 		const { type, id } = await activitypub.helpers.resolveObjects(subject.id);
 | |
| 		try {
 | |
| 			await flags.create(activitypub.helpers.mapToLocalType(type), id, actor, content);
 | |
| 		} catch (e) {
 | |
| 			reject('Flag', objects[index], actor);
 | |
| 		}
 | |
| 	}));
 | |
| };
 | |
| 
 | |
| inbox.reject = async (req) => {
 | |
| 	const { actor, object } = req.body;
 | |
| 	const { type, id } = object;
 | |
| 	const { hostname } = new URL(actor);
 | |
| 	const queueId = `${type}:${id}:${hostname}`;
 | |
| 
 | |
| 	// stop retrying rejected requests
 | |
| 	await Promise.all([
 | |
| 		db.sortedSetRemove('ap:retry:queue', queueId),
 | |
| 		db.delete(`ap:retry:queue:${queueId}`),
 | |
| 	]);
 | |
| };
 |