mirror of
				https://github.com/NodeBB/NodeBB.git
				synced 2025-10-31 19:15:58 +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) { | ||||
| 		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 + '"]'); | ||||
| 		votes.html(data.post.votes).attr('data-votes', data.post.votes); | ||||
| @@ -225,10 +225,10 @@ define('forum/topic/events', [ | ||||
| 	function togglePostVote(data) { | ||||
| 		const post = $('[data-pid="' + data.post.pid + '"]'); | ||||
| 		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); | ||||
| 		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); | ||||
| 	} | ||||
|  | ||||
|   | ||||
| @@ -77,7 +77,7 @@ define('forum/topic/votes', [ | ||||
|  | ||||
| 		const method = currentState ? 'del' : 'put'; | ||||
| 		const pid = post.attr('data-pid'); | ||||
| 		api[method](`/posts/${pid}/vote`, { | ||||
| 		api[method](`/posts/${encodeURIComponent(pid)}/vote`, { | ||||
| 			delta: delta, | ||||
| 		}, function (err) { | ||||
| 			if (err) { | ||||
|   | ||||
| @@ -94,7 +94,7 @@ Helpers.resolveLocalUid = async (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)[1]; | ||||
| 			const [type, value] = pathname.replace(nconf.get('relative_path'), '').split('/').filter(Boolean); | ||||
| 			if (type === 'uid') { | ||||
| 				return value; | ||||
| 			} | ||||
| @@ -111,3 +111,17 @@ Helpers.resolveLocalUid = async (input) => { | ||||
|  | ||||
| 	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 user = require('../user'); | ||||
| const posts = require('../posts'); | ||||
| const activitypub = require('.'); | ||||
|  | ||||
| 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) => { | ||||
| 	// Sanity checks | ||||
| 	const localUid = await helpers.resolveLocalUid(req.body.object); | ||||
| @@ -119,20 +127,29 @@ inbox.undo = async (req) => { | ||||
| 	const { actor, object } = req.body; | ||||
| 	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); | ||||
| 	if (!assertion) { | ||||
| 		throw new Error('[[error:activitypub.invalid-id]]'); | ||||
| 	} | ||||
|  | ||||
| 	if (type === 'Follow') { | ||||
| 	switch (type) { | ||||
| 		case 'Follow': { | ||||
| 			const uid = await helpers.resolveLocalUid(object.object); | ||||
| 			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 }) => { | ||||
| 	const result = await activitypub.helpers.query(uid); | ||||
| 	if (!result) { | ||||
| @@ -112,3 +113,45 @@ activitypubApi.update.note = async (caller, { post }) => { | ||||
|  | ||||
| 	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) { | ||||
| 	const api = require('.'); | ||||
| 	const result = await posts[command](data.pid, caller.uid); | ||||
| 	if (result && eventName) { | ||||
| 		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') { | ||||
| 		socketHelpers.upvote(result, notification); | ||||
| 		api.activitypub.like.note(caller, { pid: data.pid }); | ||||
| 	} else if (result && notification) { | ||||
| 		socketHelpers.sendNotificationToPostOwner(data.pid, caller.uid, command, notification); | ||||
| 	} else if (result && command === 'unvote') { | ||||
| 		socketHelpers.rescindUpvoteNotification(data.pid, caller.uid); | ||||
| 		api.activitypub.undo.like(caller, { pid: data.pid }); | ||||
| 	} | ||||
| 	return result; | ||||
| } | ||||
|   | ||||
| @@ -93,6 +93,11 @@ Controller.postInbox = async (req, res) => { | ||||
| 			break; | ||||
| 		} | ||||
|  | ||||
| 		case 'Like': { | ||||
| 			await activitypub.inbox.like(req); | ||||
| 			break; | ||||
| 		} | ||||
|  | ||||
| 		case 'Follow': { | ||||
| 			await activitypub.inbox.follow(req); | ||||
| 			break; | ||||
|   | ||||
| @@ -8,6 +8,7 @@ const topics = require('../topics'); | ||||
| const plugins = require('../plugins'); | ||||
| const privileges = require('../privileges'); | ||||
| const translator = require('../translator'); | ||||
| const utils = require('../utils'); | ||||
|  | ||||
| module.exports = function (Posts) { | ||||
| 	const votesInProgress = {}; | ||||
| @@ -99,17 +100,17 @@ module.exports = function (Posts) { | ||||
| 	}; | ||||
|  | ||||
| 	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) { | ||||
| 		votesInProgress[uid] = votesInProgress[uid] || []; | ||||
| 		votesInProgress[uid].push(parseInt(pid, 10)); | ||||
| 		votesInProgress[uid].push(String(pid)); | ||||
| 	} | ||||
|  | ||||
| 	function clearVoteProgress(pid, uid) { | ||||
| 		if (Array.isArray(votesInProgress[uid])) { | ||||
| 			const index = votesInProgress[uid].indexOf(parseInt(pid, 10)); | ||||
| 			const index = votesInProgress[uid].indexOf(String(pid)); | ||||
| 			if (index !== -1) { | ||||
| 				votesInProgress[uid].splice(index, 1); | ||||
| 			} | ||||
| @@ -171,8 +172,7 @@ module.exports = function (Posts) { | ||||
| 	} | ||||
|  | ||||
| 	async function vote(type, unvote, pid, uid, voteStatus) { | ||||
| 		uid = parseInt(uid, 10); | ||||
| 		if (uid <= 0) { | ||||
| 		if (utils.isNumber(uid) && parseInt(uid, 10) <= 0) { | ||||
| 			throw new Error('[[error:not-logged-in]]'); | ||||
| 		} | ||||
| 		const now = Date.now(); | ||||
|   | ||||
| @@ -153,6 +153,12 @@ privsCategories.can = async function (privilege, cid, uid) { | ||||
| 	if (!cid) { | ||||
| 		return false; | ||||
| 	} | ||||
|  | ||||
| 	// temporary | ||||
| 	if (cid === -1) { | ||||
| 		return true; | ||||
| 	} | ||||
|  | ||||
| 	const [disabled, isAdmin, isAllowed] = await Promise.all([ | ||||
| 		categories.getCategoryField(cid, 'disabled'), | ||||
| 		user.isAdministrator(uid), | ||||
|   | ||||
		Reference in New Issue
	
	Block a user