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