mirror of
https://github.com/NodeBB/NodeBB.git
synced 2026-01-10 01:23:04 +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);
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
activitypub.feps.announce(object.id, req.body);
|
||||
@@ -253,6 +253,7 @@ inbox.like = async (req) => {
|
||||
|
||||
inbox.announce = async (req) => {
|
||||
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);
|
||||
timestamp = timestamp.toString() !== 'Invalid Date' ? timestamp.getTime() : Date.now();
|
||||
|
||||
@@ -270,53 +271,70 @@ inbox.announce = async (req) => {
|
||||
cid = Array.from(cids)[0];
|
||||
}
|
||||
|
||||
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))) {
|
||||
throw new Error('[[error:activitypub.invalid-id]]');
|
||||
switch(true) {
|
||||
case object.type === 'Like': {
|
||||
const id = object.object.id || object.object;
|
||||
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;
|
||||
tid = await posts.getPostField(id, 'tid');
|
||||
case activitypub._constants.acceptedPostTypes.includes(object.type): {
|
||||
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');
|
||||
} else { // Remote object
|
||||
// Follower check
|
||||
if (!cid) {
|
||||
const { followers } = await activitypub.actors.getLocalFollowCounts(actor);
|
||||
if (!followers) {
|
||||
winston.verbose(`[activitypub/inbox.announce] Rejecting ${object.id} via ${actor} due to no followers`);
|
||||
reject('Announce', object, actor);
|
||||
return;
|
||||
pid = id;
|
||||
tid = await posts.getPostField(id, 'tid');
|
||||
|
||||
socketHelpers.sendNotificationToPostOwner(pid, actor, 'announce', 'notifications:activitypub.announce');
|
||||
} else { // Remote object
|
||||
// Follower check
|
||||
if (!cid) {
|
||||
const { followers } = await activitypub.actors.getLocalFollowCounts(actor);
|
||||
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._baseUrl = 'https://example.org';
|
||||
|
||||
Helpers.mocks.person = (override = {}) => {
|
||||
const baseUrl = 'https://example.org';
|
||||
const uuid = utils.generateUUID();
|
||||
let id = `${baseUrl}/${uuid}`;
|
||||
let id = `${Helpers.mocks._baseUrl}/${uuid}`;
|
||||
if (override.hasOwnProperty('id')) {
|
||||
id = override.id;
|
||||
}
|
||||
@@ -66,9 +67,8 @@ Helpers.mocks.group = (override = {}) => {
|
||||
};
|
||||
|
||||
Helpers.mocks.note = (override = {}) => {
|
||||
const baseUrl = 'https://example.org';
|
||||
const uuid = utils.generateUUID();
|
||||
const id = `${baseUrl}/object/${uuid}`;
|
||||
const id = `${Helpers.mocks._baseUrl}/object/${uuid}`;
|
||||
const note = {
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
id,
|
||||
@@ -97,9 +97,8 @@ Helpers.mocks.note = (override = {}) => {
|
||||
|
||||
Helpers.mocks.create = (object) => {
|
||||
// object is optional, will generate a public note if undefined
|
||||
const baseUrl = 'https://example.org';
|
||||
const uuid = utils.generateUUID();
|
||||
const id = `${baseUrl}/activity/${uuid}`;
|
||||
const id = `${Helpers.mocks._baseUrl}/activity/${uuid}`;
|
||||
|
||||
object = object || Helpers.mocks.note().note;
|
||||
const activity = {
|
||||
@@ -118,9 +117,8 @@ Helpers.mocks.create = (object) => {
|
||||
};
|
||||
|
||||
Helpers.mocks.accept = (actor, object) => {
|
||||
const baseUrl = 'https://example.org';
|
||||
const uuid = utils.generateUUID();
|
||||
const id = `${baseUrl}/activity/${uuid}`;
|
||||
const id = `${Helpers.mocks._baseUrl}/activity/${uuid}`;
|
||||
|
||||
const activity = {
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
@@ -132,4 +130,49 @@ Helpers.mocks.accept = (actor, object) => {
|
||||
};
|
||||
|
||||
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);
|
||||
assert.strictEqual(unread, 0);
|
||||
})
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -328,7 +328,7 @@ describe('Notes', () => {
|
||||
it('should federate out an activity with object of type "Note"', () => {
|
||||
assert(activity.object && activity.object.type);
|
||||
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', () => {
|
||||
let cid;
|
||||
let uid;
|
||||
|
||||
Reference in New Issue
Block a user