feat: new ap mocks, now publishing user outboxes

This commit is contained in:
Julian Lam
2026-03-11 12:34:57 -04:00
parent b835eef91f
commit f848393e6e
3 changed files with 143 additions and 41 deletions

View File

@@ -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',

View File

@@ -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) => {

View File

@@ -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,
});
};