mirror of
https://github.com/NodeBB/NodeBB.git
synced 2026-02-26 16:41:21 +01:00
feat: #13255, proper handling of upvotes shared by group actors
fixes #13320
This commit is contained in:
@@ -240,11 +240,11 @@ inbox.like = async (req) => {
|
|||||||
|
|
||||||
const allowed = await privileges.posts.can('posts:upvote', id, activitypub._constants.uid);
|
const allowed = await privileges.posts.can('posts:upvote', id, activitypub._constants.uid);
|
||||||
if (!allowed) {
|
if (!allowed) {
|
||||||
winston.verbose(`[activitypub/inbox.like] ${id} not allowed to be upvoted.`);
|
activitypub.helpers.log(`[activitypub/inbox.like] ${id} not allowed to be upvoted.`);
|
||||||
return reject('Like', object, actor);
|
return reject('Like', object, actor);
|
||||||
}
|
}
|
||||||
|
|
||||||
winston.verbose(`[activitypub/inbox/like] id ${id} via ${actor}`);
|
activitypub.helpers.log(`[activitypub/inbox/like] id ${id} via ${actor}`);
|
||||||
|
|
||||||
const result = await posts.upvote(id, actor);
|
const result = await posts.upvote(id, actor);
|
||||||
activitypub.feps.announce(object.id, req.body);
|
activitypub.feps.announce(object.id, req.body);
|
||||||
@@ -253,6 +253,7 @@ inbox.like = async (req) => {
|
|||||||
|
|
||||||
inbox.announce = async (req) => {
|
inbox.announce = async (req) => {
|
||||||
const { actor, object, published, to, cc } = req.body;
|
const { actor, object, published, to, cc } = req.body;
|
||||||
|
activitypub.helpers.log(`[activitypub/inbox/announce] Parsing Announce(${object.type}) from ${actor}`);
|
||||||
let timestamp = new Date(published);
|
let timestamp = new Date(published);
|
||||||
timestamp = timestamp.toString() !== 'Invalid Date' ? timestamp.getTime() : Date.now();
|
timestamp = timestamp.toString() !== 'Invalid Date' ? timestamp.getTime() : Date.now();
|
||||||
|
|
||||||
@@ -270,53 +271,70 @@ inbox.announce = async (req) => {
|
|||||||
cid = Array.from(cids)[0];
|
cid = Array.from(cids)[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (String(object.id).startsWith(nconf.get('url'))) { // Local object
|
switch(true) {
|
||||||
const { type, id } = await activitypub.helpers.resolveLocalId(object.id);
|
case object.type === 'Like': {
|
||||||
if (type !== 'post' || !(await posts.exists(id))) {
|
const id = object.object.id || object.object;
|
||||||
throw new Error('[[error:activitypub.invalid-id]]');
|
const { id: localId } = await activitypub.helpers.resolveLocalId(id);
|
||||||
|
const exists = await posts.exists(localId || id);
|
||||||
|
if (exists) {
|
||||||
|
const result = await posts.upvote(localId || id, object.actor);
|
||||||
|
if (localId) {
|
||||||
|
socketHelpers.upvote(result, 'notifications:upvoted-your-post-in');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
pid = id;
|
case activitypub._constants.acceptedPostTypes.includes(object.type): {
|
||||||
tid = await posts.getPostField(id, 'tid');
|
if (String(object.id).startsWith(nconf.get('url'))) { // Local object
|
||||||
|
const { type, id } = await activitypub.helpers.resolveLocalId(object.id);
|
||||||
|
if (type !== 'post' || !(await posts.exists(id))) {
|
||||||
|
reject('Announce', object, actor);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
socketHelpers.sendNotificationToPostOwner(pid, actor, 'announce', 'notifications:activitypub.announce');
|
pid = id;
|
||||||
} else { // Remote object
|
tid = await posts.getPostField(id, 'tid');
|
||||||
// Follower check
|
|
||||||
if (!cid) {
|
socketHelpers.sendNotificationToPostOwner(pid, actor, 'announce', 'notifications:activitypub.announce');
|
||||||
const { followers } = await activitypub.actors.getLocalFollowCounts(actor);
|
} else { // Remote object
|
||||||
if (!followers) {
|
// Follower check
|
||||||
winston.verbose(`[activitypub/inbox.announce] Rejecting ${object.id} via ${actor} due to no followers`);
|
if (!cid) {
|
||||||
reject('Announce', object, actor);
|
const { followers } = await activitypub.actors.getLocalFollowCounts(actor);
|
||||||
return;
|
if (!followers) {
|
||||||
|
winston.verbose(`[activitypub/inbox.announce] Rejecting ${object.id} via ${actor} due to no followers`);
|
||||||
|
reject('Announce', object, actor);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle case where Announce(Create(Note-ish)) is received
|
||||||
|
if (object.type === 'Create' && activitypub._constants.acceptedPostTypes.includes(object.object.type)) {
|
||||||
|
pid = object.object.id;
|
||||||
|
} else {
|
||||||
|
pid = object.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
pid = await activitypub.resolveId(0, pid); // in case wrong id is passed-in; unlikely, but still.
|
||||||
|
if (!pid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const assertion = await activitypub.notes.assert(0, pid, { cid, skipChecks: true });
|
||||||
|
if (!assertion) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
({ tid } = assertion);
|
||||||
|
await activitypub.notes.updateLocalRecipients(pid, { to, cc });
|
||||||
|
await activitypub.notes.syncUserInboxes(tid);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!cid) { // Topic events from actors followed by users only
|
||||||
|
await activitypub.notes.announce.add(pid, actor, timestamp);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle case where Announce(Create(Note-ish)) is received
|
|
||||||
if (object.type === 'Create' && activitypub._constants.acceptedPostTypes.includes(object.object.type)) {
|
|
||||||
pid = object.object.id;
|
|
||||||
} else {
|
|
||||||
pid = object.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
pid = await activitypub.resolveId(0, pid); // in case wrong id is passed-in; unlikely, but still.
|
|
||||||
if (!pid) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const assertion = await activitypub.notes.assert(0, pid, { cid });
|
|
||||||
if (!assertion) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
({ tid } = assertion);
|
|
||||||
await activitypub.notes.updateLocalRecipients(pid, { to, cc });
|
|
||||||
await activitypub.notes.syncUserInboxes(tid);
|
|
||||||
}
|
|
||||||
|
|
||||||
winston.verbose(`[activitypub/inbox/announce] Parsing id ${pid}`);
|
|
||||||
|
|
||||||
if (!cid) { // Topic events from actors followed by users only
|
|
||||||
await activitypub.notes.announce.add(pid, actor, timestamp);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -8,10 +8,11 @@ const Helpers = module.exports;
|
|||||||
|
|
||||||
Helpers.mocks = {};
|
Helpers.mocks = {};
|
||||||
|
|
||||||
|
Helpers.mocks._baseUrl = 'https://example.org';
|
||||||
|
|
||||||
Helpers.mocks.person = (override = {}) => {
|
Helpers.mocks.person = (override = {}) => {
|
||||||
const baseUrl = 'https://example.org';
|
|
||||||
const uuid = utils.generateUUID();
|
const uuid = utils.generateUUID();
|
||||||
let id = `${baseUrl}/${uuid}`;
|
let id = `${Helpers.mocks._baseUrl}/${uuid}`;
|
||||||
if (override.hasOwnProperty('id')) {
|
if (override.hasOwnProperty('id')) {
|
||||||
id = override.id;
|
id = override.id;
|
||||||
}
|
}
|
||||||
@@ -66,9 +67,8 @@ Helpers.mocks.group = (override = {}) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
Helpers.mocks.note = (override = {}) => {
|
Helpers.mocks.note = (override = {}) => {
|
||||||
const baseUrl = 'https://example.org';
|
|
||||||
const uuid = utils.generateUUID();
|
const uuid = utils.generateUUID();
|
||||||
const id = `${baseUrl}/object/${uuid}`;
|
const id = `${Helpers.mocks._baseUrl}/object/${uuid}`;
|
||||||
const note = {
|
const note = {
|
||||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||||
id,
|
id,
|
||||||
@@ -97,9 +97,8 @@ Helpers.mocks.note = (override = {}) => {
|
|||||||
|
|
||||||
Helpers.mocks.create = (object) => {
|
Helpers.mocks.create = (object) => {
|
||||||
// object is optional, will generate a public note if undefined
|
// object is optional, will generate a public note if undefined
|
||||||
const baseUrl = 'https://example.org';
|
|
||||||
const uuid = utils.generateUUID();
|
const uuid = utils.generateUUID();
|
||||||
const id = `${baseUrl}/activity/${uuid}`;
|
const id = `${Helpers.mocks._baseUrl}/activity/${uuid}`;
|
||||||
|
|
||||||
object = object || Helpers.mocks.note().note;
|
object = object || Helpers.mocks.note().note;
|
||||||
const activity = {
|
const activity = {
|
||||||
@@ -118,9 +117,8 @@ Helpers.mocks.create = (object) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
Helpers.mocks.accept = (actor, object) => {
|
Helpers.mocks.accept = (actor, object) => {
|
||||||
const baseUrl = 'https://example.org';
|
|
||||||
const uuid = utils.generateUUID();
|
const uuid = utils.generateUUID();
|
||||||
const id = `${baseUrl}/activity/${uuid}`;
|
const id = `${Helpers.mocks._baseUrl}/activity/${uuid}`;
|
||||||
|
|
||||||
const activity = {
|
const activity = {
|
||||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||||
@@ -132,4 +130,49 @@ Helpers.mocks.accept = (actor, object) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return { activity };
|
return { activity };
|
||||||
}
|
};
|
||||||
|
|
||||||
|
Helpers.mocks.like = (override = {}) => {
|
||||||
|
let actor = override.actor;
|
||||||
|
let object = override.object;
|
||||||
|
if (!actor) {
|
||||||
|
({ id: actor } = Helpers.mocks.person());
|
||||||
|
}
|
||||||
|
if (!object) {
|
||||||
|
({ id: object } = Helpers.mocks.note());
|
||||||
|
}
|
||||||
|
|
||||||
|
const activity = {
|
||||||
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||||
|
id: `${Helpers.mocks._baseUrl}/like/${encodeURIComponent(object)}`,
|
||||||
|
type: 'Like',
|
||||||
|
actor,
|
||||||
|
object,
|
||||||
|
};
|
||||||
|
|
||||||
|
return { activity };
|
||||||
|
};
|
||||||
|
|
||||||
|
Helpers.mocks.announce = (override = {}) => {
|
||||||
|
let actor = override.actor;
|
||||||
|
let object = override.object;
|
||||||
|
if (!actor) {
|
||||||
|
({ id: actor } = Helpers.mocks.person());
|
||||||
|
}
|
||||||
|
if (!object) {
|
||||||
|
({ id: object } = Helpers.mocks.note());
|
||||||
|
}
|
||||||
|
|
||||||
|
const activity = {
|
||||||
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||||
|
id: `${Helpers.mocks._baseUrl}/announce/${encodeURIComponent(object.id || object)}`,
|
||||||
|
type: 'Announce',
|
||||||
|
to: [activitypub._constants.publicAddress],
|
||||||
|
cc: [`${actor}/followers`],
|
||||||
|
actor,
|
||||||
|
object,
|
||||||
|
};
|
||||||
|
|
||||||
|
return { activity };
|
||||||
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -208,7 +208,7 @@ describe('Notes', () => {
|
|||||||
|
|
||||||
const unread = await topics.getTotalUnread(uid);
|
const unread = await topics.getTotalUnread(uid);
|
||||||
assert.strictEqual(unread, 0);
|
assert.strictEqual(unread, 0);
|
||||||
})
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -328,7 +328,7 @@ describe('Notes', () => {
|
|||||||
it('should federate out an activity with object of type "Note"', () => {
|
it('should federate out an activity with object of type "Note"', () => {
|
||||||
assert(activity.object && activity.object.type);
|
assert(activity.object && activity.object.type);
|
||||||
assert.strictEqual(activity.object.type, 'Note');
|
assert.strictEqual(activity.object.type, 'Note');
|
||||||
})
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -411,6 +411,142 @@ describe('Notes', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe.only('Inbox handling', () => {
|
||||||
|
describe('helper self-check', () => {
|
||||||
|
it('should generate a Like activity', () => {
|
||||||
|
const object = utils.generateUUID();
|
||||||
|
const { id: actor } = helpers.mocks.person();
|
||||||
|
const { activity } = helpers.mocks.like({
|
||||||
|
object,
|
||||||
|
actor,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepStrictEqual(activity, {
|
||||||
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||||
|
id: `${helpers.mocks._baseUrl}/like/${encodeURIComponent(object)}`,
|
||||||
|
type: 'Like',
|
||||||
|
actor,
|
||||||
|
object,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should generate an Announce activity wrapping a Like activity', () => {
|
||||||
|
const object = utils.generateUUID();
|
||||||
|
const { id: actor } = helpers.mocks.person();
|
||||||
|
const { activity: like } = helpers.mocks.like({
|
||||||
|
object,
|
||||||
|
actor,
|
||||||
|
});
|
||||||
|
const { id: gActor } = helpers.mocks.group();
|
||||||
|
const { activity } = helpers.mocks.announce({
|
||||||
|
actor: gActor,
|
||||||
|
object: like,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepStrictEqual(activity, {
|
||||||
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||||
|
id: `${helpers.mocks._baseUrl}/announce/${encodeURIComponent(like.id)}`,
|
||||||
|
type: 'Announce',
|
||||||
|
to: [ 'https://www.w3.org/ns/activitystreams#Public' ],
|
||||||
|
cc: [
|
||||||
|
`${gActor}/followers`,
|
||||||
|
],
|
||||||
|
actor: gActor,
|
||||||
|
object: like,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Announce', () => {
|
||||||
|
let cid;
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
({ cid } = await categories.create({ name: utils.generateUUID().slice(0, 8) }));
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('(Note)', () => {
|
||||||
|
it('should create a new topic in cid -1 if category not addressed', async () => {
|
||||||
|
const { note } = helpers.mocks.note();
|
||||||
|
await activitypub.actors.assert([note.attributedTo]);
|
||||||
|
const { activity } = helpers.mocks.announce({
|
||||||
|
object: note,
|
||||||
|
});
|
||||||
|
const uid = await user.create({ username: utils.generateUUID().slice(0, 10) });
|
||||||
|
await db.sortedSetAdd(`followersRemote:${activity.actor}`, Date.now(), uid);
|
||||||
|
|
||||||
|
const beforeCount = await db.sortedSetCard(`cid:-1:tids`);
|
||||||
|
await activitypub.inbox.announce({ body: activity });
|
||||||
|
const count = await db.sortedSetCard(`cid:-1:tids`);
|
||||||
|
|
||||||
|
assert.strictEqual(count, beforeCount + 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create a new topic in local category', async () => {
|
||||||
|
const { note } = helpers.mocks.note({
|
||||||
|
cc: [`${nconf.get('url')}/category/${cid}`],
|
||||||
|
});
|
||||||
|
await activitypub.actors.assert([note.attributedTo]);
|
||||||
|
const { activity } = helpers.mocks.announce({
|
||||||
|
object: note,
|
||||||
|
});
|
||||||
|
const uid = await user.create({ username: utils.generateUUID().slice(0, 10) });
|
||||||
|
await db.sortedSetAdd(`followersRemote:${activity.actor}`, Date.now(), uid);
|
||||||
|
|
||||||
|
const beforeCount = await db.sortedSetCard(`cid:${cid}:tids`);
|
||||||
|
await activitypub.inbox.announce({ body: activity });
|
||||||
|
const count = await db.sortedSetCard(`cid:${cid}:tids`);
|
||||||
|
|
||||||
|
assert.strictEqual(count, beforeCount + 1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('(Like)', () => {
|
||||||
|
it('should upvote a local post', async () => {
|
||||||
|
const uid = await user.create({ username: utils.generateUUID().slice(0, 10) });
|
||||||
|
const { postData } = await topics.post({
|
||||||
|
cid,
|
||||||
|
uid,
|
||||||
|
title: utils.generateUUID(),
|
||||||
|
content: utils.generateUUID(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { activity: like } = helpers.mocks.like({
|
||||||
|
object: `${nconf.get('url')}/post/${postData.pid}`,
|
||||||
|
});
|
||||||
|
const { activity } = helpers.mocks.announce({
|
||||||
|
object: like,
|
||||||
|
});
|
||||||
|
|
||||||
|
let { upvotes } = await posts.getPostFields(postData.pid, 'upvotes');
|
||||||
|
assert.strictEqual(upvotes, 0);
|
||||||
|
|
||||||
|
await activitypub.inbox.announce({ body: activity });
|
||||||
|
({ upvotes } = await posts.getPostFields(postData.pid, 'upvotes'));
|
||||||
|
assert.strictEqual(upvotes, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should upvote an asserted remote post', async () => {
|
||||||
|
const { id } = helpers.mocks.note();
|
||||||
|
await activitypub.notes.assert(0, [id], { skipChecks: true });
|
||||||
|
const { activity: like } = helpers.mocks.like({
|
||||||
|
object: id,
|
||||||
|
});
|
||||||
|
const { activity } = helpers.mocks.announce({
|
||||||
|
object: like,
|
||||||
|
});
|
||||||
|
|
||||||
|
let { upvotes } = await posts.getPostFields(id, 'upvotes');
|
||||||
|
assert.strictEqual(upvotes, 0);
|
||||||
|
|
||||||
|
await activitypub.inbox.announce({ body: activity });
|
||||||
|
|
||||||
|
({ upvotes } = await posts.getPostFields(id, 'upvotes'));
|
||||||
|
assert.strictEqual(upvotes, 1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('Inbox Synchronization', () => {
|
describe('Inbox Synchronization', () => {
|
||||||
let cid;
|
let cid;
|
||||||
let uid;
|
let uid;
|
||||||
|
|||||||
Reference in New Issue
Block a user