mirror of
				https://github.com/NodeBB/NodeBB.git
				synced 2025-10-31 02:55:58 +01:00 
			
		
		
		
	refactor: activitypub sending to handle signed requests from categories
This commit is contained in:
		| @@ -30,13 +30,13 @@ Actors.assert = async (ids, options = {}) => { | ||||
|  | ||||
| 	const actors = await Promise.all(ids.map(async (id) => { | ||||
| 		try { | ||||
| 			const actor = (typeof id === 'object' && id.hasOwnProperty('id')) ? id : await activitypub.get(0, id); | ||||
| 			const actor = (typeof id === 'object' && id.hasOwnProperty('id')) ? id : await activitypub.get('uid', 0, id); | ||||
|  | ||||
| 			// Follow counts | ||||
| 			try { | ||||
| 				const [followers, following] = await Promise.all([ | ||||
| 					actor.followers ? activitypub.get(0, actor.followers) : { totalItems: 0 }, | ||||
| 					actor.following ? activitypub.get(0, actor.following) : { totalItems: 0 }, | ||||
| 					actor.followers ? activitypub.get('uid', 0, actor.followers) : { totalItems: 0 }, | ||||
| 					actor.following ? activitypub.get('uid', 0, actor.following) : { totalItems: 0 }, | ||||
| 				]); | ||||
| 				actor.followerCount = followers.totalItems; | ||||
| 				actor.followingCount = following.totalItems; | ||||
| @@ -46,7 +46,7 @@ Actors.assert = async (ids, options = {}) => { | ||||
| 			} | ||||
|  | ||||
| 			// Post count | ||||
| 			const outbox = actor.outbox ? await activitypub.get(0, actor.outbox) : { totalItems: 0 }; | ||||
| 			const outbox = actor.outbox ? await activitypub.get('uid', 0, actor.outbox) : { totalItems: 0 }; | ||||
| 			actor.postcount = outbox.totalItems; | ||||
|  | ||||
| 			return actor; | ||||
|   | ||||
| @@ -63,8 +63,8 @@ Helpers.query = async (id) => { | ||||
| 	return { username, hostname, actorUri, publicKey }; | ||||
| }; | ||||
|  | ||||
| Helpers.generateKeys = async (uid) => { | ||||
| 	winston.verbose(`[activitypub] Generating RSA key-pair for uid ${uid}`); | ||||
| Helpers.generateKeys = async (type, id) => { | ||||
| 	winston.verbose(`[activitypub] Generating RSA key-pair for ${type} ${id}`); | ||||
| 	const { | ||||
| 		publicKey, | ||||
| 		privateKey, | ||||
| @@ -80,47 +80,41 @@ Helpers.generateKeys = async (uid) => { | ||||
| 		}, | ||||
| 	}); | ||||
|  | ||||
| 	await db.setObject(`uid:${uid}:keys`, { publicKey, privateKey }); | ||||
| 	await db.setObject(`${type}:${id}:keys`, { publicKey, privateKey }); | ||||
| 	return { publicKey, privateKey }; | ||||
| }; | ||||
|  | ||||
| Helpers.resolveLocalUid = async (input) => { | ||||
| 	let slug; | ||||
| 	const protocols = ['https']; | ||||
| 	if (process.env.CI === 'true') { | ||||
| 		protocols.push('http'); | ||||
| 	} | ||||
| Helpers.resolveLocalId = async (input) => { | ||||
| 	if (Helpers.isUri(input)) { | ||||
| 		const { host, pathname } = new URL(input); | ||||
|  | ||||
| 		if (host === nconf.get('url_parsed').host) { | ||||
| 			const [type, value] = pathname.replace(nconf.get('relative_path'), '').split('/').filter(Boolean); | ||||
| 			if (type === 'uid') { | ||||
| 				return value; | ||||
| 			const [prefix, value] = pathname.replace(nconf.get('relative_path'), '').split('/').filter(Boolean); | ||||
|  | ||||
| 			switch (prefix) { | ||||
| 				case 'uid': | ||||
| 					return { type: 'user', id: value }; | ||||
|  | ||||
| 				case 'post': | ||||
| 					return { type: 'post', id: value }; | ||||
|  | ||||
| 				case 'category': | ||||
| 					return { type: 'category', id: value }; | ||||
|  | ||||
| 				case 'user': { | ||||
| 					const uid = await user.getUidByUserslug(value); | ||||
| 					return { type: 'user', id: uid }; | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			slug = value; | ||||
| 			throw new Error('[[error:activitypub.invalid-id]]'); | ||||
| 		} else { | ||||
| 			throw new Error('[[error:activitypub.invalid-id]]'); | ||||
| 		} | ||||
| 	} else if (input.indexOf('@') !== -1) { // Webfinger | ||||
| 		([slug] = input.replace(/^acct:/, '').split('@')); | ||||
| 	} else { | ||||
| 		throw new Error('[[error:activitypub.invalid-id]]'); | ||||
| 	} | ||||
|  | ||||
| 	return await user.getUidByUserslug(slug); | ||||
| }; | ||||
|  | ||||
| Helpers.resolveLocalPid = async (uri) => { | ||||
| 	const { host, pathname } = new URL(uri); | ||||
| 	if (host === nconf.get('url_parsed').host) { | ||||
| 		const [type, value] = pathname.replace(nconf.get('relative_path'), '').split('/').filter(Boolean); | ||||
| 		if (type !== 'post') { | ||||
| 			throw new Error('[[error:activitypub.invalid-id]]'); | ||||
| 		} | ||||
|  | ||||
| 		return value; | ||||
| 		const [slug] = input.replace(/^acct:/, '').split('@'); | ||||
| 		const uid = await user.getUidByUserslug(slug); | ||||
| 		return { type: 'user', id: uid }; | ||||
| 	} | ||||
|  | ||||
| 	throw new Error('[[error:activitypub.invalid-id]]'); | ||||
|   | ||||
| @@ -5,6 +5,7 @@ const winston = require('winston'); | ||||
| const db = require('../database'); | ||||
| const user = require('../user'); | ||||
| const posts = require('../posts'); | ||||
| const categories = require('../categories'); | ||||
| const activitypub = require('.'); | ||||
|  | ||||
| const helpers = require('./helpers'); | ||||
| @@ -56,16 +57,19 @@ inbox.update = async (req) => { | ||||
|  | ||||
| inbox.like = async (req) => { | ||||
| 	const { actor, object } = req.body; | ||||
| 	const pid = await activitypub.helpers.resolveLocalPid(object); | ||||
| 	const { type, id } = await activitypub.helpers.resolveLocalId(object); | ||||
| 	if (type !== 'post' || await posts.exists(id)) { | ||||
| 		throw new Error('[[error:activitypub.invalid-id]]'); | ||||
| 	} | ||||
|  | ||||
| 	await posts.upvote(pid, actor); | ||||
| 	await posts.upvote(id, actor); | ||||
| }; | ||||
|  | ||||
| inbox.follow = async (req) => { | ||||
| 	// Sanity checks | ||||
| 	const localUid = await helpers.resolveLocalUid(req.body.object); | ||||
| 	if (!localUid) { | ||||
| 		throw new Error('[[error:invalid-uid]]'); | ||||
| 	const { type, id } = await helpers.resolveLocalId(req.body.object); | ||||
| 	if (!['category', 'user'].includes(type)) { | ||||
| 		throw new Error('[[error:activitypub.invalid-id]]'); | ||||
| 	} | ||||
|  | ||||
| 	const assertion = await activitypub.actors.assert(req.body.actor); | ||||
| @@ -73,24 +77,53 @@ inbox.follow = async (req) => { | ||||
| 		throw new Error('[[error:activitypub.invalid-id]]'); | ||||
| 	} | ||||
|  | ||||
| 	const isFollowed = await inbox.isFollowed(req.body.actor, localUid); | ||||
| 	if (type === 'user') { | ||||
| 		const exists = await user.exists(id); | ||||
| 		if (!exists) { | ||||
| 			throw new Error('[[error:invalid-uid]]'); | ||||
| 		} | ||||
|  | ||||
| 		const isFollowed = await inbox.isFollowed(req.body.actor, id); | ||||
| 		if (isFollowed) { | ||||
| 			// No additional parsing required | ||||
| 			return; | ||||
| 		} | ||||
|  | ||||
| 		const now = Date.now(); | ||||
| 	await db.sortedSetAdd(`followersRemote:${localUid}`, now, req.body.actor); | ||||
| 	await activitypub.send(localUid, req.body.actor, { | ||||
| 		await db.sortedSetAdd(`followersRemote:${id}`, now, req.body.actor); | ||||
|  | ||||
| 		const followerRemoteCount = await db.sortedSetCard(`followersRemote:${id}`); | ||||
| 		await user.setUserField(id, 'followerRemoteCount', followerRemoteCount); | ||||
|  | ||||
| 		await activitypub.send('uid', id, req.body.actor, { | ||||
| 			type: 'Accept', | ||||
| 			object: { | ||||
| 				type: 'Follow', | ||||
| 				actor: req.body.actor, | ||||
| 			}, | ||||
| 		}); | ||||
| 	} else if (type === 'category') { | ||||
| 		const exists = await categories.exists(id); | ||||
| 		if (!exists) { | ||||
| 			throw new Error('[[error:invalid-cid]]'); | ||||
| 		} | ||||
|  | ||||
| 	const followerRemoteCount = await db.sortedSetCard(`followersRemote:${localUid}`); | ||||
| 	await user.setUserField(localUid, 'followerRemoteCount', followerRemoteCount); | ||||
| 		const watchState = await categories.getWatchState([id], req.body.actor); | ||||
| 		if (watchState === categories.watchStates.tracking) { | ||||
| 			// No additional parsing required | ||||
| 			return; | ||||
| 		} | ||||
|  | ||||
| 		await user.setCategoryWatchState(req.body.actor, id, categories.watchStates.tracking); | ||||
|  | ||||
| 		await activitypub.send('cid', id, req.body.actor, { | ||||
| 			type: 'Accept', | ||||
| 			object: { | ||||
| 				type: 'Follow', | ||||
| 				actor: req.body.actor, | ||||
| 			}, | ||||
| 		}); | ||||
| 	} | ||||
| }; | ||||
|  | ||||
| inbox.isFollowed = async (actorId, uid) => { | ||||
| @@ -104,8 +137,8 @@ inbox.accept = async (req) => { | ||||
| 	const { actor, object } = req.body; | ||||
| 	const { type } = object; | ||||
|  | ||||
| 	const uid = await helpers.resolveLocalUid(object.actor); | ||||
| 	if (!uid) { | ||||
| 	const { type: localType, id: uid } = await helpers.resolveLocalId(object.actor); | ||||
| 	if (localType !== 'user' || !uid) { | ||||
| 		throw new Error('[[error:invalid-uid]]'); | ||||
| 	} | ||||
|  | ||||
| @@ -124,6 +157,7 @@ inbox.accept = async (req) => { | ||||
| }; | ||||
|  | ||||
| inbox.undo = async (req) => { | ||||
| 	// todo: "actor" in this case should be the one in object, no? | ||||
| 	const { actor, object } = req.body; | ||||
| 	const { type } = object; | ||||
|  | ||||
| @@ -132,23 +166,45 @@ inbox.undo = async (req) => { | ||||
| 		throw new Error('[[error:activitypub.invalid-id]]'); | ||||
| 	} | ||||
|  | ||||
| 	const { type: localType, id } = await helpers.resolveLocalId(object.object); | ||||
|  | ||||
| 	switch (type) { | ||||
| 		case 'Follow': { | ||||
| 			const uid = await helpers.resolveLocalUid(object.object); | ||||
| 			if (!uid) { | ||||
| 			switch (localType) { | ||||
| 				case 'user': { | ||||
| 					const exists = await user.exists(id); | ||||
| 					if (!exists) { | ||||
| 						throw new Error('[[error:invalid-uid]]'); | ||||
| 					} | ||||
|  | ||||
| 					await Promise.all([ | ||||
| 				db.sortedSetRemove(`followersRemote:${uid}`, actor), | ||||
| 				db.decrObjectField(`user:${uid}`, 'followerRemoteCount'), | ||||
| 						db.sortedSetRemove(`followersRemote:${id}`, actor), | ||||
| 						db.decrObjectField(`user:${id}`, 'followerRemoteCount'), | ||||
| 					]); | ||||
| 					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 pid = await helpers.resolveLocalPid(object.object); | ||||
| 			await posts.unvote(pid, actor); | ||||
| 			const exists = await posts.exists(id); | ||||
| 			if (localType !== 'post' || !exists) { | ||||
| 				throw new Error('[[error:invalid-pid]]'); | ||||
| 			} | ||||
|  | ||||
| 			await posts.unvote(id, actor); | ||||
| 			break; | ||||
| 		} | ||||
| 	} | ||||
|   | ||||
| @@ -37,28 +37,40 @@ ActivityPub.resolveInboxes = async (ids) => { | ||||
| 	return Array.from(inboxes); | ||||
| }; | ||||
|  | ||||
| ActivityPub.getPublicKey = async (uid) => { | ||||
| ActivityPub.getPublicKey = async (type, id) => { | ||||
| 	let publicKey; | ||||
|  | ||||
| 	try { | ||||
| 		({ publicKey } = await db.getObject(`uid:${uid}:keys`)); | ||||
| 		({ publicKey } = await db.getObject(`uid:${id}:keys`)); | ||||
| 	} catch (e) { | ||||
| 		({ publicKey } = await ActivityPub.helpers.generateKeys(uid)); | ||||
| 		({ publicKey } = await ActivityPub.helpers.generateKeys(type, id)); | ||||
| 	} | ||||
|  | ||||
| 	return publicKey; | ||||
| }; | ||||
|  | ||||
| ActivityPub.getPrivateKey = async (uid) => { | ||||
| ActivityPub.getPrivateKey = async (type, id) => { | ||||
| 	// Sanity checking | ||||
| 	if (!['cid', 'uid'].includes(type) || !utils.isNumber(id) || parseInt(id, 10) < 0) { | ||||
| 		throw new Error('[[error:invalid-data]]'); | ||||
| 	} | ||||
| 	id = parseInt(id, 10); | ||||
| 	let privateKey; | ||||
|  | ||||
| 	try { | ||||
| 		({ privateKey } = await db.getObject(`uid:${uid}:keys`)); | ||||
| 		({ privateKey } = await db.getObject(`${type}:${id}:keys`)); | ||||
| 	} catch (e) { | ||||
| 		({ privateKey } = await ActivityPub.helpers.generateKeys(uid)); | ||||
| 		({ privateKey } = await ActivityPub.helpers.generateKeys(type, id)); | ||||
| 	} | ||||
|  | ||||
| 	return privateKey; | ||||
| 	let keyId; | ||||
| 	if (type === 'uid') { | ||||
| 		keyId = `${nconf.get('url')}${id > 0 ? `/uid/${id}` : '/actor'}#key`; | ||||
| 	} else { | ||||
| 		keyId = `${nconf.get('url')}/category/${id}#key`; | ||||
| 	} | ||||
|  | ||||
| 	return { key: privateKey, keyId }; | ||||
| }; | ||||
|  | ||||
| ActivityPub.fetchPublicKey = async (uri) => { | ||||
| @@ -76,18 +88,10 @@ ActivityPub.fetchPublicKey = async (uri) => { | ||||
| 	return body.publicKey; | ||||
| }; | ||||
|  | ||||
| ActivityPub.sign = async (uid, url, payload) => { | ||||
| 	// Sanity checking | ||||
| 	if (!utils.isNumber(uid) || parseInt(uid, 10) < 0) { | ||||
| 		throw new Error('[[error:invalid-uid]]'); | ||||
| 	} | ||||
| 	uid = parseInt(uid, 10); | ||||
|  | ||||
| ActivityPub.sign = async ({ key, keyId }, url, payload) => { | ||||
| 	// Returns string for use in 'Signature' header | ||||
| 	const { host, pathname } = new URL(url); | ||||
| 	const date = new Date().toUTCString(); | ||||
| 	const key = await ActivityPub.getPrivateKey(uid); | ||||
| 	const keyId = `${nconf.get('url')}${uid > 0 ? `/uid/${uid}` : '/actor'}#key`; | ||||
| 	let digest = null; | ||||
|  | ||||
| 	let headers = '(request-target) host date'; | ||||
| @@ -156,13 +160,14 @@ ActivityPub.verify = async (req) => { | ||||
| 	} | ||||
| }; | ||||
|  | ||||
| ActivityPub.get = async (uid, uri) => { | ||||
| 	const cacheKey = [uid, uri].join(';'); | ||||
| ActivityPub.get = async (type, id, uri) => { | ||||
| 	const cacheKey = [id, uri].join(';'); | ||||
| 	if (requestCache.has(cacheKey)) { | ||||
| 		return requestCache.get(cacheKey); | ||||
| 	} | ||||
|  | ||||
| 	const headers = uid >= 0 ? await ActivityPub.sign(uid, uri) : {}; | ||||
| 	const keyData = await ActivityPub.getPrivateKey(type, id); | ||||
| 	const headers = id >= 0 ? await ActivityPub.sign(keyData, uri) : {}; | ||||
| 	winston.verbose(`[activitypub/get] ${uri}`); | ||||
| 	const { response, body } = await request.get(uri, { | ||||
| 		headers: { | ||||
| @@ -184,7 +189,7 @@ ActivityPub.get = async (uid, uri) => { | ||||
| 	return body; | ||||
| }; | ||||
|  | ||||
| ActivityPub.send = async (uid, targets, payload) => { | ||||
| ActivityPub.send = async (type, id, targets, payload) => { | ||||
| 	if (!Array.isArray(targets)) { | ||||
| 		targets = [targets]; | ||||
| 	} | ||||
| @@ -193,12 +198,14 @@ ActivityPub.send = async (uid, targets, payload) => { | ||||
|  | ||||
| 	payload = { | ||||
| 		'@context': 'https://www.w3.org/ns/activitystreams', | ||||
| 		actor: `${nconf.get('url')}/uid/${uid}`, | ||||
| 		actor: `${nconf.get('url')}/uid/${id}`, | ||||
| 		...payload, | ||||
| 	}; | ||||
|  | ||||
| 	await Promise.all(inboxes.map(async (uri) => { | ||||
| 		const headers = await ActivityPub.sign(uid, uri, payload); | ||||
| 		const keyData = await ActivityPub.getPrivateKey(type, id); | ||||
| 		const headers = await ActivityPub.sign(keyData, uri, payload); | ||||
| 		winston.verbose(`[activitypub/send] ${uri}`); | ||||
| 		const { response } = await request.post(uri, { | ||||
| 			headers: { | ||||
| 				...headers, | ||||
|   | ||||
| @@ -118,7 +118,7 @@ Mocks.actors = {}; | ||||
|  | ||||
| Mocks.actors.user = async (uid) => { | ||||
| 	let { username, userslug, displayname: name, aboutme, picture, 'cover:url': cover } = await user.getUserData(uid); | ||||
| 	const publicKey = await activitypub.getPublicKey(uid); | ||||
| 	const publicKey = await activitypub.getPublicKey('uid', uid); | ||||
|  | ||||
| 	if (picture) { | ||||
| 		const imagePath = await user.getLocalAvatarPath(uid); | ||||
|   | ||||
| @@ -12,7 +12,7 @@ const activitypub = module.parent.exports; | ||||
| const Notes = module.exports; | ||||
|  | ||||
| Notes.resolveId = async (uid, id) => { | ||||
| 	({ id } = await activitypub.get(uid, id)); | ||||
| 	({ id } = await activitypub.get('uid', uid, id)); | ||||
| 	return id; | ||||
| }; | ||||
|  | ||||
| @@ -30,7 +30,7 @@ Notes.assert = async (uid, input, options = {}) => { | ||||
| 			let postData; | ||||
| 			winston.verbose(`[activitypub/notes.assert] Not found, saving note to database`); | ||||
| 			if (activitypub.helpers.isUri(item)) { | ||||
| 				const object = await activitypub.get(uid, item); | ||||
| 				const object = await activitypub.get('uid', uid, item); | ||||
| 				postData = await activitypub.mocks.post(object); | ||||
| 			} else { | ||||
| 				postData = item; | ||||
| @@ -60,7 +60,7 @@ Notes.getParentChain = async (uid, input) => { | ||||
| 				await traverse(uid, postData.toPid); | ||||
| 			} | ||||
| 		} else { | ||||
| 			let object = await activitypub.get(uid, id); | ||||
| 			let object = await activitypub.get('uid', uid, id); | ||||
| 			object = await activitypub.mocks.post(object); | ||||
| 			if (object) { | ||||
| 				chain.add(object); | ||||
|   | ||||
| @@ -22,7 +22,7 @@ activitypubApi.follow = async (caller, { uid } = {}) => { | ||||
| 		throw new Error('[[error:activitypub.invalid-id]]'); | ||||
| 	} | ||||
|  | ||||
| 	await activitypub.send(caller.uid, [result.actorUri], { | ||||
| 	await activitypub.send('uid', caller.uid, [result.actorUri], { | ||||
| 		type: 'Follow', | ||||
| 		object: result.actorUri, | ||||
| 	}); | ||||
| @@ -35,7 +35,7 @@ activitypubApi.unfollow = async (caller, { uid }) => { | ||||
| 		throw new Error('[[error:activitypub.invalid-id]]'); | ||||
| 	} | ||||
|  | ||||
| 	await activitypub.send(caller.uid, [result.actorUri], { | ||||
| 	await activitypub.send('uid', caller.uid, [result.actorUri], { | ||||
| 		type: 'Undo', | ||||
| 		object: { | ||||
| 			type: 'Follow', | ||||
| @@ -81,7 +81,7 @@ activitypubApi.create.post = async (caller, { pid }) => { | ||||
| 		object, | ||||
| 	}; | ||||
|  | ||||
| 	await activitypub.send(caller.uid, Array.from(targets), payload); | ||||
| 	await activitypub.send('uid', caller.uid, Array.from(targets), payload); | ||||
| }; | ||||
|  | ||||
| activitypubApi.update = {}; | ||||
| @@ -92,7 +92,7 @@ activitypubApi.update.profile = async (caller, { uid }) => { | ||||
| 		db.getSortedSetMembers(`followersRemote:${caller.uid}`), | ||||
| 	]); | ||||
|  | ||||
| 	await activitypub.send(caller.uid, followers, { | ||||
| 	await activitypub.send('uid', caller.uid, followers, { | ||||
| 		type: 'Update', | ||||
| 		to: [activitypub._constants.publicAddress], | ||||
| 		cc: [], | ||||
| @@ -111,7 +111,7 @@ activitypubApi.update.note = async (caller, { post }) => { | ||||
| 		object, | ||||
| 	}; | ||||
|  | ||||
| 	await activitypub.send(caller.uid, Array.from(targets), payload); | ||||
| 	await activitypub.send('uid', caller.uid, Array.from(targets), payload); | ||||
| }; | ||||
|  | ||||
| activitypubApi.like = {}; | ||||
| @@ -126,7 +126,7 @@ activitypubApi.like.note = async (caller, { pid }) => { | ||||
| 		return; | ||||
| 	} | ||||
|  | ||||
| 	await activitypub.send(caller.uid, [uid], { | ||||
| 	await activitypub.send('uid', caller.uid, [uid], { | ||||
| 		type: 'Like', | ||||
| 		object: pid, | ||||
| 	}); | ||||
| @@ -146,7 +146,7 @@ activitypubApi.undo.like = async (caller, { pid }) => { | ||||
| 		return; | ||||
| 	} | ||||
|  | ||||
| 	await activitypub.send(caller.uid, [uid], { | ||||
| 	await activitypub.send('uid', caller.uid, [uid], { | ||||
| 		type: 'Undo', | ||||
| 		object: { | ||||
| 			actor: `${nconf.get('url')}/uid/${caller.uid}`, | ||||
|   | ||||
| @@ -2,6 +2,7 @@ | ||||
|  | ||||
| const db = require('../database'); | ||||
| const user = require('../user'); | ||||
| const activitypub = require('../activitypub'); | ||||
|  | ||||
| module.exports = function (Categories) { | ||||
| 	Categories.watchStates = { | ||||
| @@ -20,7 +21,7 @@ module.exports = function (Categories) { | ||||
| 	}; | ||||
|  | ||||
| 	Categories.getWatchState = async function (cids, uid) { | ||||
| 		if (!(parseInt(uid, 10) > 0)) { | ||||
| 		if (!activitypub.helpers.isUri(uid) && !(parseInt(uid, 10) > 0)) { | ||||
| 			return cids.map(() => Categories.watchStates.notwatching); | ||||
| 		} | ||||
| 		if (!Array.isArray(cids) || !cids.length) { | ||||
|   | ||||
| @@ -10,7 +10,7 @@ const activitypub = require('../../activitypub'); | ||||
| const Actors = module.exports; | ||||
|  | ||||
| Actors.application = async function (req, res) { | ||||
| 	const publicKey = await activitypub.getPublicKey(0); | ||||
| 	const publicKey = await activitypub.getPublicKey('uid', 0); | ||||
| 	const name = meta.config.title || 'NodeBB'; | ||||
|  | ||||
| 	res.status(200).json({ | ||||
|   | ||||
| @@ -4,11 +4,12 @@ const _ = require('lodash'); | ||||
|  | ||||
| const db = require('../database'); | ||||
| const categories = require('../categories'); | ||||
| const activitypub = require('../activitypub'); | ||||
| const plugins = require('../plugins'); | ||||
|  | ||||
| module.exports = function (User) { | ||||
| 	User.setCategoryWatchState = async function (uid, cids, state) { | ||||
| 		if (!(parseInt(uid, 10) > 0)) { | ||||
| 		if (!activitypub.helpers.isUri(uid) && !(parseInt(uid, 10) > 0)) { | ||||
| 			return; | ||||
| 		} | ||||
| 		const isStateValid = Object.values(categories.watchStates).includes(parseInt(state, 10)); | ||||
|   | ||||
| @@ -5,6 +5,7 @@ const validator = require('validator'); | ||||
|  | ||||
| const meta = require('../meta'); | ||||
| const db = require('../database'); | ||||
| const activitypub = require('../activitypub'); | ||||
| const plugins = require('../plugins'); | ||||
| const notifications = require('../notifications'); | ||||
| const languages = require('../languages'); | ||||
| @@ -16,6 +17,10 @@ module.exports = function (User) { | ||||
| 		postsPerPage: 20, | ||||
| 		topicsPerPage: 20, | ||||
| 	}; | ||||
| 	const remoteDefaultSettings = Object.freeze({ | ||||
| 		categoryWatchState: 'notwatching', | ||||
| 	}); | ||||
|  | ||||
| 	User.getSettings = async function (uid) { | ||||
| 		if (parseInt(uid, 10) <= 0) { | ||||
| 			const isSpider = parseInt(uid, 10) === -1; | ||||
| @@ -90,6 +95,8 @@ module.exports = function (User) { | ||||
| 	function getSetting(settings, key, defaultValue) { | ||||
| 		if (settings[key] || settings[key] === 0) { | ||||
| 			return settings[key]; | ||||
| 		} else if (activitypub.helpers.isUri(settings.uid) && remoteDefaultSettings[key]) { | ||||
| 			return remoteDefaultSettings[key]; | ||||
| 		} else if (meta.config[key] || meta.config[key] === 0) { | ||||
| 			return meta.config[key]; | ||||
| 		} | ||||
|   | ||||
| @@ -88,7 +88,7 @@ describe('ActivityPub integration', () => { | ||||
|  | ||||
| 		}); | ||||
|  | ||||
| 		describe('.resolveLocalUid()', () => { | ||||
| 		describe('.resolveLocalId()', () => { | ||||
| 			let uid; | ||||
| 			let slug; | ||||
|  | ||||
| @@ -99,29 +99,29 @@ describe('ActivityPub integration', () => { | ||||
|  | ||||
| 			it('should throw when an invalid input is passed in', async () => { | ||||
| 				await assert.rejects( | ||||
| 					activitypub.helpers.resolveLocalUid('ncl28h3qwhoiclwnevoinw3u'), | ||||
| 					activitypub.helpers.resolveLocalId('ncl28h3qwhoiclwnevoinw3u'), | ||||
| 					{ message: '[[error:activitypub.invalid-id]]' } | ||||
| 				); | ||||
| 			}); | ||||
|  | ||||
| 			it('should return null when valid input is passed but does not resolve', async () => { | ||||
| 				const uid = await activitypub.helpers.resolveLocalUid(`acct:foobar@${nconf.get('url_parsed').host}`); | ||||
| 				assert.strictEqual(uid, null); | ||||
| 				const { id } = await activitypub.helpers.resolveLocalId(`acct:foobar@${nconf.get('url_parsed').host}`); | ||||
| 				assert.strictEqual(id, null); | ||||
| 			}); | ||||
|  | ||||
| 			it('should resolve to a local uid when given a webfinger-style string', async () => { | ||||
| 				const found = await activitypub.helpers.resolveLocalUid(`acct:${slug}@${nconf.get('url_parsed').host}`); | ||||
| 				assert.strictEqual(found, uid); | ||||
| 				const { id } = await activitypub.helpers.resolveLocalId(`acct:${slug}@${nconf.get('url_parsed').host}`); | ||||
| 				assert.strictEqual(id, uid); | ||||
| 			}); | ||||
|  | ||||
| 			it('should resolve even without the "acct:" prefix', async () => { | ||||
| 				const found = await activitypub.helpers.resolveLocalUid(`${slug}@${nconf.get('url_parsed').host}`); | ||||
| 				assert.strictEqual(found, uid); | ||||
| 				const { id } = await activitypub.helpers.resolveLocalId(`${slug}@${nconf.get('url_parsed').host}`); | ||||
| 				assert.strictEqual(id, uid); | ||||
| 			}); | ||||
|  | ||||
| 			it('should resolve when passed a full URL', async () => { | ||||
| 				const found = await activitypub.helpers.resolveLocalUid(`${nconf.get('url')}/user/${slug}`); | ||||
| 				assert.strictEqual(found, uid); | ||||
| 				const { id } = await activitypub.helpers.resolveLocalId(`${nconf.get('url')}/user/${slug}`); | ||||
| 				assert.strictEqual(id, uid); | ||||
| 			}); | ||||
| 		}); | ||||
| 	}); | ||||
| @@ -274,7 +274,8 @@ describe('ActivityPub integration', () => { | ||||
|  | ||||
| 			it('should create a key-pair for a user if the user does not have one already', async () => { | ||||
| 				const endpoint = `${nconf.get('url')}/uid/${uid}/inbox`; | ||||
| 				await activitypub.sign(uid, endpoint); | ||||
| 				const keyData = await activitypub.getPrivateKey('uid', uid); | ||||
| 				await activitypub.sign(keyData, endpoint); | ||||
| 				const { publicKey, privateKey } = await db.getObject(`uid:${uid}:keys`); | ||||
|  | ||||
| 				assert(publicKey); | ||||
| @@ -283,7 +284,8 @@ describe('ActivityPub integration', () => { | ||||
|  | ||||
| 			it('should return an object with date, a null digest, and signature, if no payload is passed in', async () => { | ||||
| 				const endpoint = `${nconf.get('url')}/uid/${uid}/inbox`; | ||||
| 				const { date, digest, signature } = await activitypub.sign(uid, endpoint); | ||||
| 				const keyData = await activitypub.getPrivateKey('uid', uid); | ||||
| 				const { date, digest, signature } = await activitypub.sign(keyData, endpoint); | ||||
| 				const dateObj = new Date(date); | ||||
|  | ||||
| 				assert(signature); | ||||
| @@ -294,7 +296,8 @@ describe('ActivityPub integration', () => { | ||||
| 			it('should also return a digest hash if payload is passed in', async () => { | ||||
| 				const endpoint = `${nconf.get('url')}/uid/${uid}/inbox`; | ||||
| 				const payload = { foo: 'bar' }; | ||||
| 				const { digest } = await activitypub.sign(uid, endpoint, payload); | ||||
| 				const keyData = await activitypub.getPrivateKey('uid', uid); | ||||
| 				const { digest } = await activitypub.sign(keyData, endpoint, payload); | ||||
| 				const hash = createHash('sha256'); | ||||
| 				hash.update(JSON.stringify(payload)); | ||||
| 				const checksum = hash.digest('base64'); | ||||
| @@ -305,7 +308,8 @@ describe('ActivityPub integration', () => { | ||||
|  | ||||
| 			it('should create a key for NodeBB itself if a uid of 0 is passed in', async () => { | ||||
| 				const endpoint = `${nconf.get('url')}/uid/${uid}/inbox`; | ||||
| 				await activitypub.sign(0, endpoint); | ||||
| 				const keyData = await activitypub.getPrivateKey('uid', 0); | ||||
| 				await activitypub.sign(keyData, endpoint); | ||||
| 				const { publicKey, privateKey } = await db.getObject(`uid:0:keys`); | ||||
|  | ||||
| 				assert(publicKey); | ||||
| @@ -314,7 +318,8 @@ describe('ActivityPub integration', () => { | ||||
|  | ||||
| 			it('should return headers with an appropriate key id uri', async () => { | ||||
| 				const endpoint = `${nconf.get('url')}/uid/${uid}/inbox`; | ||||
| 				const { signature } = await activitypub.sign(uid, endpoint); | ||||
| 				const keyData = await activitypub.getPrivateKey('uid', uid); | ||||
| 				const { signature } = await activitypub.sign(keyData, endpoint); | ||||
| 				const [keyId] = signature.split(','); | ||||
|  | ||||
| 				assert(signature); | ||||
| @@ -323,7 +328,8 @@ describe('ActivityPub integration', () => { | ||||
|  | ||||
| 			it('should return the instance key id when uid is 0', async () => { | ||||
| 				const endpoint = `${nconf.get('url')}/uid/${uid}/inbox`; | ||||
| 				const { signature } = await activitypub.sign(0, endpoint); | ||||
| 				const keyData = await activitypub.getPrivateKey('uid', 0); | ||||
| 				const { signature } = await activitypub.sign(keyData, endpoint); | ||||
| 				const [keyId] = signature.split(','); | ||||
|  | ||||
| 				assert(signature); | ||||
| @@ -355,7 +361,8 @@ describe('ActivityPub integration', () => { | ||||
| 			it('should return true when the proper signature and relevant headers are passed in', async () => { | ||||
| 				const endpoint = `${nconf.get('url')}/user/${username}/inbox`; | ||||
| 				const path = `/user/${username}/inbox`; | ||||
| 				const signature = await activitypub.sign(uid, endpoint); | ||||
| 				const keyData = await activitypub.getPrivateKey('uid', uid); | ||||
| 				const signature = await activitypub.sign(keyData, endpoint); | ||||
| 				const { host } = nconf.get('url_parsed'); | ||||
| 				const req = { | ||||
| 					...mockReqBase, | ||||
| @@ -372,7 +379,8 @@ describe('ActivityPub integration', () => { | ||||
| 			it('should return true when a digest is also passed in', async () => { | ||||
| 				const endpoint = `${nconf.get('url')}/user/${username}/inbox`; | ||||
| 				const path = `/user/${username}/inbox`; | ||||
| 				const signature = await activitypub.sign(uid, endpoint, { foo: 'bar' }); | ||||
| 				const keyData = await activitypub.getPrivateKey('uid', uid); | ||||
| 				const signature = await activitypub.sign(keyData, endpoint, { foo: 'bar' }); | ||||
| 				const { host } = nconf.get('url_parsed'); | ||||
| 				const req = { | ||||
| 					...mockReqBase, | ||||
| @@ -412,7 +420,7 @@ describe('ActivityPub integration', () => { | ||||
| 					const post = (await posts.getPostSummaryByPids([postData.pid], uid, { stripTags: false })).pop(); | ||||
| 					note = await activitypub.mocks.note(post); | ||||
|  | ||||
| 					await activitypub.send(uid, [`${nconf.get('url')}/uid/${uid}`], { | ||||
| 					await activitypub.send('uid', uid, [`${nconf.get('url')}/uid/${uid}`], { | ||||
| 						type: 'Create', | ||||
| 						object: note, | ||||
| 					}); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user