2024-01-12 15:23:30 -05:00
|
|
|
'use strict';
|
|
|
|
|
|
|
|
|
|
const winston = require('winston');
|
2024-02-16 12:07:29 -05:00
|
|
|
const crypto = require('crypto');
|
2024-01-12 15:23:30 -05:00
|
|
|
|
|
|
|
|
const db = require('../database');
|
2024-02-27 15:25:13 -05:00
|
|
|
const meta = require('../meta');
|
2024-02-26 11:39:32 -05:00
|
|
|
const privileges = require('../privileges');
|
2024-03-14 13:17:26 -04:00
|
|
|
const categories = require('../categories');
|
2024-02-12 14:34:37 -05:00
|
|
|
const user = require('../user');
|
2024-01-17 11:54:20 -05:00
|
|
|
const topics = require('../topics');
|
2024-01-12 15:23:30 -05:00
|
|
|
const posts = require('../posts');
|
2024-01-18 16:20:37 -05:00
|
|
|
const utils = require('../utils');
|
2024-01-12 15:23:30 -05:00
|
|
|
|
|
|
|
|
const activitypub = module.parent.exports;
|
|
|
|
|
const Notes = module.exports;
|
|
|
|
|
|
2024-02-12 16:23:21 -05:00
|
|
|
Notes.updateLocalRecipients = async (id, { to, cc }) => {
|
2024-02-13 12:03:16 -05:00
|
|
|
const recipients = new Set([...(to || []), ...(cc || [])]);
|
2024-02-12 14:34:37 -05:00
|
|
|
const uids = new Set();
|
|
|
|
|
await Promise.all(Array.from(recipients).map(async (recipient) => {
|
|
|
|
|
const { type, id } = await activitypub.helpers.resolveLocalId(recipient);
|
|
|
|
|
if (type === 'user' && await user.exists(id)) {
|
|
|
|
|
uids.add(parseInt(id, 10));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const followedUid = await db.getObjectField('followersUrl:uid', recipient);
|
|
|
|
|
if (followedUid) {
|
|
|
|
|
const followers = await db.getSortedSetMembers(`followersRemote:${followedUid}`);
|
|
|
|
|
if (followers.length) {
|
|
|
|
|
uids.add(...followers.map(uid => parseInt(uid, 10)));
|
|
|
|
|
}
|
|
|
|
|
// return;
|
2024-01-24 14:35:21 -05:00
|
|
|
}
|
2024-01-12 15:23:30 -05:00
|
|
|
}));
|
2024-02-12 14:34:37 -05:00
|
|
|
|
2024-02-12 16:23:21 -05:00
|
|
|
if (uids.size > 0) {
|
|
|
|
|
await db.setAdd(`post:${id}:recipients`, Array.from(uids));
|
|
|
|
|
}
|
2024-01-12 15:23:30 -05:00
|
|
|
};
|
|
|
|
|
|
2024-02-16 12:07:29 -05:00
|
|
|
Notes.saveAttachments = async (id, attachments) => {
|
2024-02-16 15:59:04 -05:00
|
|
|
if (!attachments) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-16 12:07:29 -05:00
|
|
|
const bulkOps = {
|
|
|
|
|
hash: [],
|
|
|
|
|
zset: {
|
|
|
|
|
score: [],
|
|
|
|
|
value: [],
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
2024-02-20 11:57:50 -05:00
|
|
|
attachments.filter(Boolean).forEach(({ mediaType, url, name, width, height }, idx) => {
|
2024-02-16 12:07:29 -05:00
|
|
|
if (!url) { // only required property
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const hash = crypto.createHash('sha256').update(url).digest('hex');
|
|
|
|
|
const key = `attachment:${hash}`;
|
|
|
|
|
|
|
|
|
|
bulkOps.hash.push([key, { mediaType, url, name, width, height }]);
|
|
|
|
|
bulkOps.zset.score.push(idx);
|
|
|
|
|
bulkOps.zset.value.push(hash);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await Promise.all([
|
|
|
|
|
db.setObjectBulk(bulkOps.hash),
|
|
|
|
|
db.sortedSetAdd(`post:${id}:attachments`, bulkOps.zset.score, bulkOps.zset.value),
|
|
|
|
|
]);
|
|
|
|
|
};
|
|
|
|
|
|
2024-01-12 15:23:30 -05:00
|
|
|
Notes.getParentChain = async (uid, input) => {
|
|
|
|
|
// Traverse upwards via `inReplyTo` until you find the root-level Note
|
|
|
|
|
const id = activitypub.helpers.isUri(input) ? input : input.id;
|
|
|
|
|
|
|
|
|
|
const chain = new Set();
|
|
|
|
|
const traverse = async (uid, id) => {
|
2024-02-09 11:15:03 -05:00
|
|
|
// Handle remote reference to local post
|
|
|
|
|
const { type, id: localId } = await activitypub.helpers.resolveLocalId(id);
|
|
|
|
|
if (type === 'post' && localId) {
|
2024-02-28 12:45:11 -05:00
|
|
|
return await traverse(uid, localId);
|
2024-02-09 11:15:03 -05:00
|
|
|
}
|
|
|
|
|
|
2024-01-12 15:23:30 -05:00
|
|
|
const exists = await db.exists(`post:${id}`);
|
|
|
|
|
if (exists) {
|
2024-01-12 16:39:29 -05:00
|
|
|
const postData = await posts.getPostData(id);
|
|
|
|
|
chain.add(postData);
|
|
|
|
|
if (postData.toPid) {
|
|
|
|
|
await traverse(uid, postData.toPid);
|
2024-02-28 12:45:11 -05:00
|
|
|
} else if (utils.isNumber(id)) { // local pid without toPid, could be OP or reply to OP
|
|
|
|
|
const mainPid = await topics.getTopicField(postData.tid, 'mainPid');
|
2024-03-14 13:28:54 -04:00
|
|
|
if (mainPid !== parseInt(id, 10)) {
|
2024-02-28 12:45:11 -05:00
|
|
|
await traverse(uid, mainPid);
|
|
|
|
|
}
|
2024-01-12 15:23:30 -05:00
|
|
|
}
|
|
|
|
|
} else {
|
2024-02-09 11:31:42 -05:00
|
|
|
let object;
|
|
|
|
|
try {
|
|
|
|
|
object = await activitypub.get('uid', uid, id);
|
2024-02-08 11:33:27 -05:00
|
|
|
|
2024-02-12 14:54:13 -05:00
|
|
|
// Handle incorrect id passed in
|
|
|
|
|
if (id !== object.id) {
|
|
|
|
|
return await traverse(uid, object.id);
|
|
|
|
|
}
|
2024-02-08 11:33:27 -05:00
|
|
|
|
2024-02-12 14:54:13 -05:00
|
|
|
object = await activitypub.mocks.post(object);
|
|
|
|
|
if (object) {
|
|
|
|
|
chain.add(object);
|
|
|
|
|
if (object.toPid) {
|
|
|
|
|
await traverse(uid, object.toPid);
|
|
|
|
|
}
|
2024-01-12 15:23:30 -05:00
|
|
|
}
|
2024-02-12 14:54:13 -05:00
|
|
|
} catch (e) {
|
|
|
|
|
winston.warn(`[activitypub/notes/getParentChain] Cannot retrieve ${id}, terminating here.`);
|
2024-01-12 15:23:30 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
await traverse(uid, id);
|
|
|
|
|
return chain;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Notes.assertTopic = async (uid, id) => {
|
2024-01-16 10:44:47 -05:00
|
|
|
/**
|
|
|
|
|
* Given the id of any post, traverses up to cache the entire threaded context
|
|
|
|
|
*
|
|
|
|
|
* Unfortunately, due to limitations and fragmentation of the existing ActivityPub landscape,
|
|
|
|
|
* retrieving the entire reply tree is not possible at this time.
|
|
|
|
|
*/
|
|
|
|
|
|
2024-01-12 15:23:30 -05:00
|
|
|
const chain = Array.from(await Notes.getParentChain(uid, id));
|
2024-02-12 15:25:49 -05:00
|
|
|
if (!chain.length) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-29 11:19:56 -05:00
|
|
|
const mainPost = chain[chain.length - 1];
|
|
|
|
|
let { pid: mainPid, tid, uid: authorId, timestamp, name, content } = mainPost;
|
2024-02-28 13:29:21 -05:00
|
|
|
const hasTid = !!tid;
|
|
|
|
|
|
2024-02-29 11:19:56 -05:00
|
|
|
const members = await db.isSortedSetMembers(`tid:${tid}:posts`, chain.slice(0, -1).map(p => p.pid));
|
|
|
|
|
members.push(await posts.exists(mainPid));
|
2024-01-19 11:31:04 -05:00
|
|
|
if (tid && members.every(Boolean)) {
|
2024-01-16 10:44:47 -05:00
|
|
|
// All cached, return early.
|
2024-02-29 11:19:56 -05:00
|
|
|
winston.verbose('[notes/assertTopic] No new notes to process.');
|
2024-01-16 10:44:47 -05:00
|
|
|
return tid;
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-28 13:29:21 -05:00
|
|
|
let cid;
|
|
|
|
|
let title;
|
|
|
|
|
if (hasTid) {
|
2024-03-12 13:27:29 -04:00
|
|
|
({ cid, mainPid } = await topics.getTopicFields(tid, ['tid', 'cid', 'mainPid']));
|
2024-02-28 13:29:21 -05:00
|
|
|
} else {
|
|
|
|
|
// mainPid ok to leave as-is
|
|
|
|
|
cid = -1;
|
|
|
|
|
title = name || utils.decodeHTMLEntities(utils.stripHTMLTags(content));
|
|
|
|
|
if (title.length > meta.config.maximumTitleLength) {
|
2024-03-12 13:27:29 -04:00
|
|
|
title = `${title.slice(0, meta.config.maximumTitleLength - 3)}...`;
|
2024-02-28 13:29:21 -05:00
|
|
|
}
|
|
|
|
|
}
|
2024-02-29 00:06:59 -05:00
|
|
|
mainPid = utils.isNumber(mainPid) ? parseInt(mainPid, 10) : mainPid;
|
2024-02-26 11:39:32 -05:00
|
|
|
|
|
|
|
|
// Privilege check for local categories
|
|
|
|
|
const privilege = `topics:${tid ? 'reply' : 'create'}`;
|
|
|
|
|
const allowed = await privileges.categories.can(privilege, cid, activitypub._constants.uid);
|
|
|
|
|
if (!allowed) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-27 15:25:13 -05:00
|
|
|
tid = tid || utils.generateUUID();
|
2024-02-29 11:19:56 -05:00
|
|
|
mainPost.tid = tid;
|
2024-02-27 15:25:13 -05:00
|
|
|
|
2024-03-05 14:24:13 -05:00
|
|
|
const unprocessed = chain.map((post) => {
|
|
|
|
|
post.tid = tid; // add tid to post hash
|
|
|
|
|
return post;
|
|
|
|
|
}).filter((p, idx) => !members[idx]);
|
2024-03-13 11:03:08 -04:00
|
|
|
const count = unprocessed.length;
|
|
|
|
|
winston.verbose(`[notes/assertTopic] ${count} new note(s) found.`);
|
2024-01-16 10:44:47 -05:00
|
|
|
|
2024-01-12 15:23:30 -05:00
|
|
|
const [ids, timestamps] = [
|
2024-02-28 21:50:43 -05:00
|
|
|
unprocessed.map(n => (utils.isNumber(n.pid) ? parseInt(n.pid, 10) : n.pid)),
|
2024-01-16 10:44:47 -05:00
|
|
|
unprocessed.map(n => n.timestamp),
|
2024-01-12 15:23:30 -05:00
|
|
|
];
|
|
|
|
|
|
2024-02-07 12:28:16 -05:00
|
|
|
// mainPid doesn't belong in posts zset
|
2024-02-29 00:06:59 -05:00
|
|
|
if (ids.includes(mainPid)) {
|
|
|
|
|
const idx = ids.indexOf(mainPid);
|
2024-02-28 13:29:21 -05:00
|
|
|
ids.splice(idx, 1);
|
|
|
|
|
timestamps.splice(idx, 1);
|
|
|
|
|
}
|
2024-02-07 12:28:16 -05:00
|
|
|
|
2024-02-29 11:19:56 -05:00
|
|
|
let tags;
|
|
|
|
|
if (!hasTid) {
|
2024-03-15 16:38:00 -04:00
|
|
|
const { to, cc, attachment } = mainPost._activitypub;
|
2024-03-14 13:17:26 -04:00
|
|
|
const systemTags = (meta.config.systemTags || '').split(',');
|
2024-03-14 14:48:35 -04:00
|
|
|
const maxTags = await categories.getCategoryField(cid, 'maxTags');
|
2024-03-07 15:11:43 -05:00
|
|
|
tags = (mainPost._activitypub.tag || [])
|
2024-03-14 13:41:04 -04:00
|
|
|
.filter(o => o.type === 'Hashtag' && !systemTags.includes(o.name.slice(1)))
|
2024-03-07 15:11:43 -05:00
|
|
|
.map(o => o.name.slice(1));
|
2024-03-12 13:27:29 -04:00
|
|
|
|
2024-03-14 14:48:35 -04:00
|
|
|
if (maxTags && tags.length > maxTags) {
|
2024-03-14 13:17:26 -04:00
|
|
|
tags.length = maxTags;
|
|
|
|
|
}
|
|
|
|
|
|
2024-03-15 16:38:00 -04:00
|
|
|
await Promise.all([
|
|
|
|
|
topics.post({
|
|
|
|
|
tid,
|
|
|
|
|
uid: authorId,
|
|
|
|
|
cid,
|
|
|
|
|
pid: mainPid,
|
|
|
|
|
title,
|
|
|
|
|
timestamp,
|
|
|
|
|
tags,
|
|
|
|
|
content: mainPost.content,
|
|
|
|
|
_activitypub: mainPost._activitypub,
|
|
|
|
|
}),
|
|
|
|
|
Notes.updateLocalRecipients(mainPid, { to, cc }),
|
|
|
|
|
Notes.saveAttachments(mainPid, attachment),
|
|
|
|
|
]);
|
2024-03-12 13:27:29 -04:00
|
|
|
unprocessed.pop();
|
2024-02-29 11:19:56 -05:00
|
|
|
}
|
|
|
|
|
|
2024-03-12 13:27:29 -04:00
|
|
|
unprocessed.reverse();
|
|
|
|
|
for (const post of unprocessed) {
|
2024-03-15 16:38:00 -04:00
|
|
|
const { to, cc, attachment } = post._activitypub;
|
|
|
|
|
|
2024-03-12 13:27:29 -04:00
|
|
|
// eslint-disable-next-line no-await-in-loop
|
2024-03-15 16:38:00 -04:00
|
|
|
await Promise.all([
|
|
|
|
|
topics.reply(post),
|
|
|
|
|
Notes.updateLocalRecipients(post.pid, { to, cc }),
|
|
|
|
|
Notes.saveAttachments(post.pid, attachment),
|
|
|
|
|
]);
|
2024-03-12 13:27:29 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await Notes.syncUserInboxes(tid);
|
2024-01-12 15:23:30 -05:00
|
|
|
|
2024-03-13 11:03:08 -04:00
|
|
|
return { tid, count };
|
2024-01-12 15:23:30 -05:00
|
|
|
};
|
|
|
|
|
|
2024-02-12 14:34:37 -05:00
|
|
|
Notes.syncUserInboxes = async function (tid) {
|
2024-02-12 14:51:21 -05:00
|
|
|
const [pids, { cid, mainPid }] = await Promise.all([
|
|
|
|
|
db.getSortedSetMembers(`tid:${tid}:posts`),
|
2024-02-12 14:59:13 -05:00
|
|
|
topics.getTopicFields(tid, ['tid', 'cid', 'mainPid']),
|
2024-02-12 14:51:21 -05:00
|
|
|
]);
|
|
|
|
|
pids.unshift(mainPid);
|
|
|
|
|
|
2024-02-12 14:34:37 -05:00
|
|
|
const recipients = await db.getSetsMembers(pids.map(id => `post:${id}:recipients`));
|
|
|
|
|
const uids = recipients.reduce((set, uids) => new Set([...set, ...uids.map(u => parseInt(u, 10))]), new Set());
|
|
|
|
|
const keys = Array.from(uids).map(uid => `uid:${uid}:inbox`);
|
|
|
|
|
const score = await db.sortedSetScore(`cid:${cid}:tids`, tid);
|
|
|
|
|
|
2024-02-12 14:41:22 -05:00
|
|
|
winston.verbose(`[activitypub/syncUserInboxes] Syncing tid ${tid} with ${uids.size} inboxes`);
|
2024-02-12 15:01:10 -05:00
|
|
|
await db.sortedSetsAdd(keys, keys.map(() => score || Date.now()), tid);
|
2024-02-12 14:34:37 -05:00
|
|
|
};
|