diff --git a/src/activitypub/mocks.js b/src/activitypub/mocks.js index ce3d1267d4..5eb0ed10d1 100644 --- a/src/activitypub/mocks.js +++ b/src/activitypub/mocks.js @@ -986,6 +986,53 @@ Mocks.activities.create = async (pid, uid, post) => { return { activity, targets }; }; +Mocks.activities.like = (pid, uid) => ({ + id: `${nconf.get('url')}/uid/${uid}#activity/like/${encodeURIComponent(pid)}`, + type: 'Like', + actor: `${nconf.get('url')}/uid/${uid}`, + object: utils.isNumber(pid) ? `${nconf.get('url')}/post/${pid}` : pid, +}); + +Mocks.activities.dislike = (pid, uid) => ({ + id: `${nconf.get('url')}/uid/${uid}#activity/dislike/${encodeURIComponent(pid)}`, + type: 'Dislike', + actor: `${nconf.get('url')}/uid/${uid}`, + object: utils.isNumber(pid) ? `${nconf.get('url')}/post/${pid}` : pid, +}); + +Mocks.activities.announce = async (tid, uid) => { + const { mainPid: pid, cid } = await topics.getTopicFields(tid, ['mainPid', 'cid']); + const authorUid = await posts.getPostField(pid, 'uid'); // author + const { to, cc, targets } = await activitypub.buildRecipients({ + id: pid, + to: [activitypub._constants.publicAddress], + }, uid ? { uid } : { cid }); + if (!utils.isNumber(authorUid)) { + cc.push(authorUid); + targets.add(authorUid); + } + + const payload = uid ? { + id: `${nconf.get('url')}/post/${encodeURIComponent(pid)}#activity/announce/uid/${uid}`, + type: 'Announce', + actor: `${nconf.get('url')}/uid/${uid}`, + } : { + id: `${nconf.get('url')}/post/${encodeURIComponent(pid)}#activity/announce/cid/${cid}`, + type: 'Announce', + actor: `${nconf.get('url')}/category/${cid}`, + }; + + return { + activity: { + ...payload, + to, + cc, + object: utils.isNumber(pid) ? `${nconf.get('url')}/post/${pid}` : pid, + }, + targets, + }; +}; + Mocks.tombstone = async properties => ({ '@context': 'https://www.w3.org/ns/activitystreams', type: 'Tombstone', diff --git a/src/activitypub/out.js b/src/activitypub/out.js index dbe3a3e8e4..d0f075bdb5 100644 --- a/src/activitypub/out.js +++ b/src/activitypub/out.js @@ -254,12 +254,7 @@ Out.delete.note = enabledCheck(async (uid, pid) => { Out.like = {}; Out.like.note = enabledCheck(async (uid, pid) => { - const payload = { - id: `${nconf.get('url')}/uid/${uid}#activity/like/${encodeURIComponent(pid)}`, - type: 'Like', - actor: `${nconf.get('url')}/uid/${uid}`, - object: utils.isNumber(pid) ? `${nconf.get('url')}/post/${pid}` : pid, - }; + const payload = activitypub.mocks.activities.like(pid, uid); if (!activitypub.helpers.isUri(pid)) { // only 1b12 announce for local likes await activitypub.feps.announce(pid, payload); @@ -280,12 +275,7 @@ Out.like.note = enabledCheck(async (uid, pid) => { Out.dislike = {}; Out.dislike.note = enabledCheck(async (uid, pid) => { - const payload = { - id: `${nconf.get('url')}/uid/${uid}#activity/dislike/${encodeURIComponent(pid)}`, - type: 'Dislike', - actor: `${nconf.get('url')}/uid/${uid}`, - object: utils.isNumber(pid) ? `${nconf.get('url')}/post/${pid}` : pid, - }; + const payload = activitypub.mocks.activities.dislike(pid, uid); if (!activitypub.helpers.isUri(pid)) { // only 1b12 announce for local likes await activitypub.feps.announce(pid, payload); @@ -320,37 +310,14 @@ Out.announce.topic = enabledCheck(async (tid, uid) => { } } - const authorUid = await posts.getPostField(pid, 'uid'); // author const allowed = await privileges.posts.can('topics:read', pid, activitypub._constants.uid); if (!allowed) { activitypub.helpers.log(`[activitypub/api] Not federating announce of pid ${pid} to the fediverse due to privileges.`); return; } - const { to, cc, targets } = await activitypub.buildRecipients({ - id: pid, - to: [activitypub._constants.publicAddress], - }, uid ? { uid } : { cid }); - if (!utils.isNumber(authorUid)) { - cc.push(authorUid); - targets.add(authorUid); - } - - const payload = uid ? { - id: `${nconf.get('url')}/post/${encodeURIComponent(pid)}#activity/announce/uid/${uid}`, - type: 'Announce', - actor: `${nconf.get('url')}/uid/${uid}`, - } : { - id: `${nconf.get('url')}/post/${encodeURIComponent(pid)}#activity/announce/cid/${cid}`, - type: 'Announce', - actor: `${nconf.get('url')}/category/${cid}`, - }; - await activitypub.send(uid ? 'uid' : 'cid', uid || cid, Array.from(targets), { - ...payload, - to, - cc, - object: utils.isNumber(pid) ? `${nconf.get('url')}/post/${pid}` : pid, - }); + const { activity, targets } = await activitypub.mocks.activities.announce(tid, uid); + await activitypub.send(uid ? 'uid' : 'cid', uid || cid, Array.from(targets), activity); }); Out.flag = enabledCheck(async (uid, flag) => { diff --git a/src/controllers/activitypub/index.js b/src/controllers/activitypub/index.js index c64ef9ef57..fb57f533c2 100644 --- a/src/controllers/activitypub/index.js +++ b/src/controllers/activitypub/index.js @@ -3,7 +3,9 @@ const nconf = require('nconf'); const winston = require('winston'); +const db = require('../../database'); const meta = require('../../meta'); +const posts = require('../../posts'); const user = require('../../user'); const activitypub = require('../../activitypub'); const utils = require('../../utils'); @@ -116,12 +118,98 @@ Controller.getFollowers = async (req, res) => { }; Controller.getOutbox = async (req, res) => { - // stub + // Posts, shares, and votes + const { uid } = req.params; + let { after, before } = req.query; + + let totalItems = await db.sortedSetsCard([`uid:${uid}:posts`, `uid:${uid}:upvote`, `uid:${uid}:downvote`, `uid:${uid}:shares`]); + totalItems = totalItems.reduce((sum, count) => { + sum += count; + return sum; + }, 0); + + const perPage = 20; + let paginate = true; + if (totalItems <= perPage) { + before = undefined; + after = undefined; + paginate = false; + } + + let prev; + let next; + const partOf = paginate && (after || before) && `${nconf.get('url')}/uid/${uid}/outbox`; + const first = paginate && !after && !before && `${nconf.get('url')}/uid/${uid}/outbox?after=${Date.now()}`; + const last = paginate && !after && !before && `${nconf.get('url')}/uid/${uid}/outbox?before=0`; + let activities; + + if (!paginate || after || before) { + const limit = after ? parseInt(after, 10) - 1 : parseInt(before, 10) + 1; + const method = after ? 'getSortedSetRevRangeByScoreWithScores' : 'getSortedSetRangeByScoreWithScores'; + + const [post, upvote, downvote, share] = await Promise.all([ + db[method](`uid:${uid}:posts`, 0, 20, limit, `${after ? '-' : '+'}inf`), + db[method](`uid:${uid}:upvote`, 0, 20, limit, `${after ? '-' : '+'}inf`), + db[method](`uid:${uid}:downvote`, 0, 20, limit, `${after ? '-' : '+'}inf`), + db[method](`uid:${uid}:shares`, 0, 20, limit, `${after ? '-' : '+'}inf`), + ]); + activities = [ + post.map(post => ({ ...post, type: 'post' })), + upvote.map(upvote => ({ ...upvote, type: 'upvote' })), + downvote.map(downvote => ({ ...downvote, type: 'downvote' })), + share.map(share => ({ ...share, type: 'share' })), + ].flat().sort((a, b) => b.score - a.score); + if (after) { + activities = activities.slice(0, 20); + } else { + activities = activities.slice(-20); + } + + if (activities.length) { + prev = `${nconf.get('url')}/uid/${uid}/outbox?before=${activities[0].score}`; + next = `${nconf.get('url')}/uid/${uid}/outbox?after=${activities[19].score}`; + + let postsData = activities.filter((({ type }) => type === 'post')); + postsData = await posts.getPostSummaryByPids(postsData.map(({ value }) => value), 0, { stripTags: false }); + postsData = postsData.reduce((map, postData) => { + map.set(postData.pid, postData); + return map; + }, new Map()); + + activities = await Promise.all(activities.map(async ({ type, value: id }) => { + switch (type) { + case 'post': { + const { activity } = await activitypub.mocks.activities.create(id, 0, postsData.get(id)); + return activity; + } + + case 'upvote': { + return activitypub.mocks.activities.like(id, uid); + } + + case 'downvote': { + return activitypub.mocks.activities.dislike(id, uid); + } + + case 'share': { + const { activity } = await activitypub.mocks.activities.announce(id, uid); + return activity; + } + } + })); + } + } + res.status(200).json({ '@context': 'https://www.w3.org/ns/activitystreams', - type: 'OrderedCollection', - totalItems: 0, - orderedItems: [], + type: paginate ? 'OrderedCollectionPage' : 'OrderedCollection', + totalItems, + ...(prev && { prev }), + ...(next && { next }), + ...(first && { first }), + ...(last && { last }), + ...(partOf && { partOf }), + orderedItems: activities, }); };