mirror of
https://github.com/NodeBB/NodeBB.git
synced 2025-10-27 09:06:15 +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) {
|
||||||
|
case 'Follow': {
|
||||||
|
const uid = await helpers.resolveLocalUid(object.object);
|
||||||
|
if (!uid) {
|
||||||
|
throw new Error('[[error:invalid-uid]]');
|
||||||
|
}
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
db.sortedSetRemove(`followersRemote:${uid}`, actor),
|
db.sortedSetRemove(`followersRemote:${uid}`, actor),
|
||||||
db.decrObjectField(`user:${uid}`, 'followerRemoteCount'),
|
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