mirror of
				https://github.com/NodeBB/NodeBB.git
				synced 2025-10-31 11:05:54 +01:00 
			
		
		
		
	feat: Like(Note) and Undo(Like); federating likes
This commit is contained in:
		| @@ -71,7 +71,7 @@ define('forum/topic/events', [ | |||||||
|  |  | ||||||
| 	function updatePostVotesAndUserReputation(data) { | 	function updatePostVotesAndUserReputation(data) { | ||||||
| 		const votes = $('[data-pid="' + data.post.pid + '"] [component="post/vote-count"]').filter(function (index, el) { | 		const votes = $('[data-pid="' + data.post.pid + '"] [component="post/vote-count"]').filter(function (index, el) { | ||||||
| 			return parseInt($(el).closest('[data-pid]').attr('data-pid'), 10) === parseInt(data.post.pid, 10); | 			return $(el).closest('[data-pid]').attr('data-pid') === String(data.post.pid); | ||||||
| 		}); | 		}); | ||||||
| 		const reputationElements = $('.reputation[data-uid="' + data.post.uid + '"]'); | 		const reputationElements = $('.reputation[data-uid="' + data.post.uid + '"]'); | ||||||
| 		votes.html(data.post.votes).attr('data-votes', data.post.votes); | 		votes.html(data.post.votes).attr('data-votes', data.post.votes); | ||||||
| @@ -225,10 +225,10 @@ define('forum/topic/events', [ | |||||||
| 	function togglePostVote(data) { | 	function togglePostVote(data) { | ||||||
| 		const post = $('[data-pid="' + data.post.pid + '"]'); | 		const post = $('[data-pid="' + data.post.pid + '"]'); | ||||||
| 		post.find('[component="post/upvote"]').filter(function (index, el) { | 		post.find('[component="post/upvote"]').filter(function (index, el) { | ||||||
| 			return parseInt($(el).closest('[data-pid]').attr('data-pid'), 10) === parseInt(data.post.pid, 10); | 			return $(el).closest('[data-pid]').attr('data-pid') === String(data.post.pid); | ||||||
| 		}).toggleClass('upvoted', data.upvote); | 		}).toggleClass('upvoted', data.upvote); | ||||||
| 		post.find('[component="post/downvote"]').filter(function (index, el) { | 		post.find('[component="post/downvote"]').filter(function (index, el) { | ||||||
| 			return parseInt($(el).closest('[data-pid]').attr('data-pid'), 10) === parseInt(data.post.pid, 10); | 			return $(el).closest('[data-pid]').attr('data-pid') === String(data.post.pid); | ||||||
| 		}).toggleClass('downvoted', data.downvote); | 		}).toggleClass('downvoted', data.downvote); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|   | |||||||
| @@ -77,7 +77,7 @@ define('forum/topic/votes', [ | |||||||
|  |  | ||||||
| 		const method = currentState ? 'del' : 'put'; | 		const method = currentState ? 'del' : 'put'; | ||||||
| 		const pid = post.attr('data-pid'); | 		const pid = post.attr('data-pid'); | ||||||
| 		api[method](`/posts/${pid}/vote`, { | 		api[method](`/posts/${encodeURIComponent(pid)}/vote`, { | ||||||
| 			delta: delta, | 			delta: delta, | ||||||
| 		}, function (err) { | 		}, function (err) { | ||||||
| 			if (err) { | 			if (err) { | ||||||
|   | |||||||
| @@ -94,7 +94,7 @@ Helpers.resolveLocalUid = async (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)[1]; | 			const [type, value] = pathname.replace(nconf.get('relative_path'), '').split('/').filter(Boolean); | ||||||
| 			if (type === 'uid') { | 			if (type === 'uid') { | ||||||
| 				return value; | 				return value; | ||||||
| 			} | 			} | ||||||
| @@ -111,3 +111,17 @@ Helpers.resolveLocalUid = async (input) => { | |||||||
|  |  | ||||||
| 	return await user.getUidByUserslug(slug); | 	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]]'); | ||||||
|  | }; | ||||||
|   | |||||||
| @@ -4,6 +4,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 activitypub = require('.'); | const activitypub = require('.'); | ||||||
|  |  | ||||||
| const helpers = require('./helpers'); | const helpers = require('./helpers'); | ||||||
| @@ -53,6 +54,13 @@ inbox.update = async (req) => { | |||||||
| 	} | 	} | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | inbox.like = async (req) => { | ||||||
|  | 	const { actor, object } = req.body; | ||||||
|  | 	const pid = await activitypub.helpers.resolveLocalPid(object); | ||||||
|  |  | ||||||
|  | 	await posts.upvote(pid, actor); | ||||||
|  | }; | ||||||
|  |  | ||||||
| inbox.follow = async (req) => { | inbox.follow = async (req) => { | ||||||
| 	// Sanity checks | 	// Sanity checks | ||||||
| 	const localUid = await helpers.resolveLocalUid(req.body.object); | 	const localUid = await helpers.resolveLocalUid(req.body.object); | ||||||
| @@ -119,20 +127,29 @@ inbox.undo = async (req) => { | |||||||
| 	const { actor, object } = req.body; | 	const { actor, object } = req.body; | ||||||
| 	const { type } = object; | 	const { type } = object; | ||||||
|  |  | ||||||
| 	const uid = await helpers.resolveLocalUid(object.object); |  | ||||||
| 	if (!uid) { |  | ||||||
| 		throw new Error('[[error:invalid-uid]]'); |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	const assertion = await activitypub.actors.assert(actor); | 	const assertion = await activitypub.actors.assert(actor); | ||||||
| 	if (!assertion) { | 	if (!assertion) { | ||||||
| 		throw new Error('[[error:activitypub.invalid-id]]'); | 		throw new Error('[[error:activitypub.invalid-id]]'); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if (type === 'Follow') { | 	switch (type) { | ||||||
| 		await Promise.all([ | 		case 'Follow': { | ||||||
| 			db.sortedSetRemove(`followersRemote:${uid}`, actor), | 			const uid = await helpers.resolveLocalUid(object.object); | ||||||
| 			db.decrObjectField(`user:${uid}`, 'followerRemoteCount'), | 			if (!uid) { | ||||||
| 		]); | 				throw new Error('[[error:invalid-uid]]'); | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			await Promise.all([ | ||||||
|  | 				db.sortedSetRemove(`followersRemote:${uid}`, actor), | ||||||
|  | 				db.decrObjectField(`user:${uid}`, 'followerRemoteCount'), | ||||||
|  | 			]); | ||||||
|  | 			break; | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		case 'Like': { | ||||||
|  | 			const pid = await helpers.resolveLocalPid(object.object); | ||||||
|  | 			await posts.unvote(pid, actor); | ||||||
|  | 			break; | ||||||
|  | 		} | ||||||
| 	} | 	} | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -28,6 +28,7 @@ activitypubApi.follow = async (caller, { uid } = {}) => { | |||||||
| 	}); | 	}); | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | // should be .undo.follow | ||||||
| activitypubApi.unfollow = async (caller, { uid }) => { | activitypubApi.unfollow = async (caller, { uid }) => { | ||||||
| 	const result = await activitypub.helpers.query(uid); | 	const result = await activitypub.helpers.query(uid); | ||||||
| 	if (!result) { | 	if (!result) { | ||||||
| @@ -112,3 +113,45 @@ activitypubApi.update.note = async (caller, { post }) => { | |||||||
|  |  | ||||||
| 	await activitypub.send(caller.uid, Array.from(targets), payload); | 	await activitypub.send(caller.uid, Array.from(targets), payload); | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | activitypubApi.like = {}; | ||||||
|  |  | ||||||
|  | activitypubApi.like.note = async (caller, { pid }) => { | ||||||
|  | 	if (!activitypub.helpers.isUri(pid)) { | ||||||
|  | 		return; | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	const uid = await posts.getPostField(pid, 'uid'); | ||||||
|  | 	if (!activitypub.helpers.isUri(uid)) { | ||||||
|  | 		return; | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	await activitypub.send(caller.uid, [uid], { | ||||||
|  | 		type: 'Like', | ||||||
|  | 		object: pid, | ||||||
|  | 	}); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | activitypubApi.undo = {}; | ||||||
|  |  | ||||||
|  | // activitypubApi.undo.follow = | ||||||
|  |  | ||||||
|  | activitypubApi.undo.like = async (caller, { pid }) => { | ||||||
|  | 	if (!activitypub.helpers.isUri(pid)) { | ||||||
|  | 		return; | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	const uid = await posts.getPostField(pid, 'uid'); | ||||||
|  | 	if (!activitypub.helpers.isUri(uid)) { | ||||||
|  | 		return; | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	await activitypub.send(caller.uid, [uid], { | ||||||
|  | 		type: 'Undo', | ||||||
|  | 		object: { | ||||||
|  | 			actor: `${nconf.get('url')}/uid/${caller.uid}`, | ||||||
|  | 			type: 'Like', | ||||||
|  | 			object: pid, | ||||||
|  | 		}, | ||||||
|  | 	}); | ||||||
|  | }; | ||||||
|   | |||||||
| @@ -129,6 +129,7 @@ exports.postCommand = async function (caller, command, eventName, notification, | |||||||
| }; | }; | ||||||
|  |  | ||||||
| async function executeCommand(caller, command, eventName, notification, data) { | async function executeCommand(caller, command, eventName, notification, data) { | ||||||
|  | 	const api = require('.'); | ||||||
| 	const result = await posts[command](data.pid, caller.uid); | 	const result = await posts[command](data.pid, caller.uid); | ||||||
| 	if (result && eventName) { | 	if (result && eventName) { | ||||||
| 		websockets.in(`uid_${caller.uid}`).emit(`posts.${command}`, result); | 		websockets.in(`uid_${caller.uid}`).emit(`posts.${command}`, result); | ||||||
| @@ -136,10 +137,12 @@ async function executeCommand(caller, command, eventName, notification, data) { | |||||||
| 	} | 	} | ||||||
| 	if (result && command === 'upvote') { | 	if (result && command === 'upvote') { | ||||||
| 		socketHelpers.upvote(result, notification); | 		socketHelpers.upvote(result, notification); | ||||||
|  | 		api.activitypub.like.note(caller, { pid: data.pid }); | ||||||
| 	} else if (result && notification) { | 	} else if (result && notification) { | ||||||
| 		socketHelpers.sendNotificationToPostOwner(data.pid, caller.uid, command, notification); | 		socketHelpers.sendNotificationToPostOwner(data.pid, caller.uid, command, notification); | ||||||
| 	} else if (result && command === 'unvote') { | 	} else if (result && command === 'unvote') { | ||||||
| 		socketHelpers.rescindUpvoteNotification(data.pid, caller.uid); | 		socketHelpers.rescindUpvoteNotification(data.pid, caller.uid); | ||||||
|  | 		api.activitypub.undo.like(caller, { pid: data.pid }); | ||||||
| 	} | 	} | ||||||
| 	return result; | 	return result; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -93,6 +93,11 @@ Controller.postInbox = async (req, res) => { | |||||||
| 			break; | 			break; | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | 		case 'Like': { | ||||||
|  | 			await activitypub.inbox.like(req); | ||||||
|  | 			break; | ||||||
|  | 		} | ||||||
|  |  | ||||||
| 		case 'Follow': { | 		case 'Follow': { | ||||||
| 			await activitypub.inbox.follow(req); | 			await activitypub.inbox.follow(req); | ||||||
| 			break; | 			break; | ||||||
|   | |||||||
| @@ -8,6 +8,7 @@ const topics = require('../topics'); | |||||||
| const plugins = require('../plugins'); | const plugins = require('../plugins'); | ||||||
| const privileges = require('../privileges'); | const privileges = require('../privileges'); | ||||||
| const translator = require('../translator'); | const translator = require('../translator'); | ||||||
|  | const utils = require('../utils'); | ||||||
|  |  | ||||||
| module.exports = function (Posts) { | module.exports = function (Posts) { | ||||||
| 	const votesInProgress = {}; | 	const votesInProgress = {}; | ||||||
| @@ -99,17 +100,17 @@ module.exports = function (Posts) { | |||||||
| 	}; | 	}; | ||||||
|  |  | ||||||
| 	function voteInProgress(pid, uid) { | 	function voteInProgress(pid, uid) { | ||||||
| 		return Array.isArray(votesInProgress[uid]) && votesInProgress[uid].includes(parseInt(pid, 10)); | 		return Array.isArray(votesInProgress[uid]) && votesInProgress[uid].includes(String(pid)); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	function putVoteInProgress(pid, uid) { | 	function putVoteInProgress(pid, uid) { | ||||||
| 		votesInProgress[uid] = votesInProgress[uid] || []; | 		votesInProgress[uid] = votesInProgress[uid] || []; | ||||||
| 		votesInProgress[uid].push(parseInt(pid, 10)); | 		votesInProgress[uid].push(String(pid)); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	function clearVoteProgress(pid, uid) { | 	function clearVoteProgress(pid, uid) { | ||||||
| 		if (Array.isArray(votesInProgress[uid])) { | 		if (Array.isArray(votesInProgress[uid])) { | ||||||
| 			const index = votesInProgress[uid].indexOf(parseInt(pid, 10)); | 			const index = votesInProgress[uid].indexOf(String(pid)); | ||||||
| 			if (index !== -1) { | 			if (index !== -1) { | ||||||
| 				votesInProgress[uid].splice(index, 1); | 				votesInProgress[uid].splice(index, 1); | ||||||
| 			} | 			} | ||||||
| @@ -171,8 +172,7 @@ module.exports = function (Posts) { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	async function vote(type, unvote, pid, uid, voteStatus) { | 	async function vote(type, unvote, pid, uid, voteStatus) { | ||||||
| 		uid = parseInt(uid, 10); | 		if (utils.isNumber(uid) && parseInt(uid, 10) <= 0) { | ||||||
| 		if (uid <= 0) { |  | ||||||
| 			throw new Error('[[error:not-logged-in]]'); | 			throw new Error('[[error:not-logged-in]]'); | ||||||
| 		} | 		} | ||||||
| 		const now = Date.now(); | 		const now = Date.now(); | ||||||
|   | |||||||
| @@ -153,6 +153,12 @@ privsCategories.can = async function (privilege, cid, uid) { | |||||||
| 	if (!cid) { | 	if (!cid) { | ||||||
| 		return false; | 		return false; | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	// temporary | ||||||
|  | 	if (cid === -1) { | ||||||
|  | 		return true; | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	const [disabled, isAdmin, isAllowed] = await Promise.all([ | 	const [disabled, isAdmin, isAllowed] = await Promise.all([ | ||||||
| 		categories.getCategoryField(cid, 'disabled'), | 		categories.getCategoryField(cid, 'disabled'), | ||||||
| 		user.isAdministrator(uid), | 		user.isAdministrator(uid), | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user