2023-06-28 14:59:39 -04:00
|
|
|
'use strict';
|
|
|
|
|
|
2024-01-16 12:00:50 -05:00
|
|
|
const winston = require('winston');
|
2024-02-07 00:14:29 -05:00
|
|
|
const nconf = require('nconf');
|
2024-01-16 12:00:50 -05:00
|
|
|
|
2023-06-28 14:59:39 -04:00
|
|
|
const db = require('../database');
|
2024-02-26 11:39:32 -05:00
|
|
|
const privileges = require('../privileges');
|
2023-06-28 14:59:39 -04:00
|
|
|
const user = require('../user');
|
2024-02-01 15:59:29 -05:00
|
|
|
const posts = require('../posts');
|
2024-02-06 14:57:44 -05:00
|
|
|
const topics = require('../topics');
|
2024-02-05 16:57:17 -05:00
|
|
|
const categories = require('../categories');
|
2024-02-29 16:09:57 -05:00
|
|
|
const notifications = require('../notifications');
|
2024-02-07 14:29:47 -05:00
|
|
|
const utils = require('../utils');
|
2023-12-13 13:15:03 -05:00
|
|
|
const activitypub = require('.');
|
2023-06-28 14:59:39 -04:00
|
|
|
|
2024-02-29 16:09:57 -05:00
|
|
|
const socketHelpers = require('../socket.io/helpers');
|
2023-06-28 14:59:39 -04:00
|
|
|
const helpers = require('./helpers');
|
|
|
|
|
|
|
|
|
|
const inbox = module.exports;
|
|
|
|
|
|
2024-01-16 12:00:50 -05:00
|
|
|
inbox.create = async (req) => {
|
|
|
|
|
const { object } = req.body;
|
|
|
|
|
|
2024-02-21 14:58:52 -05:00
|
|
|
// Temporary, reject non-public notes.
|
2024-02-26 11:39:32 -05:00
|
|
|
if (![...object.to, ...object.cc].includes(activitypub._constants.publicAddress)) {
|
2024-02-21 14:58:52 -05:00
|
|
|
throw new Error('[[error:activitypub.not-implemented]]');
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-26 11:39:32 -05:00
|
|
|
const tid = await activitypub.notes.assertTopic(0, object.id);
|
|
|
|
|
winston.info(`[activitypub/inbox] Parsing note ${object.id} into topic ${tid}`);
|
2024-01-16 13:55:58 -05:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
inbox.update = async (req) => {
|
2024-01-30 12:11:10 -05:00
|
|
|
const { actor, object } = req.body;
|
|
|
|
|
|
|
|
|
|
// Origin checking
|
|
|
|
|
const actorHostname = new URL(actor).hostname;
|
|
|
|
|
const objectHostname = new URL(object.id).hostname;
|
|
|
|
|
if (actorHostname !== objectHostname) {
|
|
|
|
|
throw new Error('[[error:activitypub.origin-mismatch]]');
|
|
|
|
|
}
|
2024-01-16 13:55:58 -05:00
|
|
|
|
2024-01-26 16:48:16 -05:00
|
|
|
switch (object.type) {
|
|
|
|
|
case 'Note': {
|
2024-02-28 13:14:15 -05:00
|
|
|
const [exists, allowed] = await Promise.all([
|
|
|
|
|
posts.exists(object.id),
|
|
|
|
|
privileges.posts.can('posts:edit', object.id, activitypub._constants.uid),
|
|
|
|
|
]);
|
|
|
|
|
if (!exists || !allowed) {
|
|
|
|
|
winston.info(`[activitypub/inbox.update] ${object.id} not allowed to be edited.`);
|
|
|
|
|
return activitypub.send('uid', 0, actor, {
|
|
|
|
|
type: 'Reject',
|
|
|
|
|
object,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-26 16:48:16 -05:00
|
|
|
const postData = await activitypub.mocks.post(object);
|
|
|
|
|
|
|
|
|
|
if (postData) {
|
|
|
|
|
await activitypub.notes.assert(0, [postData], { update: true });
|
|
|
|
|
winston.info(`[activitypub/inbox.update] Updating note ${postData.pid}`);
|
|
|
|
|
} else {
|
|
|
|
|
winston.warn(`[activitypub/inbox.update] Received note did not parse properly (id: ${object.id})`);
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case 'Person': {
|
|
|
|
|
await activitypub.actors.assert(object, { update: true });
|
|
|
|
|
break;
|
|
|
|
|
}
|
2024-01-16 12:00:50 -05:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2024-02-01 15:59:29 -05:00
|
|
|
inbox.like = async (req) => {
|
|
|
|
|
const { actor, object } = req.body;
|
2024-02-05 16:57:17 -05:00
|
|
|
const { type, id } = await activitypub.helpers.resolveLocalId(object);
|
2024-02-06 15:20:30 -05:00
|
|
|
if (type !== 'post' || !(await posts.exists(id))) {
|
2024-02-05 16:57:17 -05:00
|
|
|
throw new Error('[[error:activitypub.invalid-id]]');
|
|
|
|
|
}
|
2024-02-01 15:59:29 -05:00
|
|
|
|
2024-02-26 11:39:32 -05:00
|
|
|
const allowed = await privileges.posts.can('posts:upvote', id, activitypub._constants.uid);
|
|
|
|
|
if (!allowed) {
|
|
|
|
|
winston.info(`[activitypub/inbox.like] ${id} not allowed to be upvoted.`);
|
|
|
|
|
return activitypub.send('uid', 0, actor, {
|
|
|
|
|
type: 'Reject',
|
|
|
|
|
object,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-07 12:50:26 -05:00
|
|
|
winston.info(`[activitypub/inbox/like] id ${id} via ${actor}`);
|
|
|
|
|
|
2024-02-29 16:09:57 -05:00
|
|
|
const result = await posts.upvote(id, actor);
|
|
|
|
|
socketHelpers.upvote(result, 'notifications:upvoted-your-post-in');
|
2024-02-01 15:59:29 -05:00
|
|
|
};
|
|
|
|
|
|
2024-02-06 14:57:44 -05:00
|
|
|
inbox.announce = async (req) => {
|
2024-02-12 16:23:21 -05:00
|
|
|
const { actor, object, published, to, cc } = req.body;
|
2024-02-13 12:03:16 -05:00
|
|
|
let timestamp = new Date(published);
|
2024-02-14 10:23:06 -05:00
|
|
|
timestamp = timestamp.toString() !== 'Invalid Date' ? timestamp.getTime() : Date.now();
|
2024-02-06 14:57:44 -05:00
|
|
|
|
|
|
|
|
const assertion = await activitypub.actors.assert(actor);
|
|
|
|
|
if (!assertion) {
|
|
|
|
|
throw new Error('[[error:activitypub.invalid-id]]');
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-07 12:28:27 -05:00
|
|
|
let tid;
|
|
|
|
|
let pid;
|
|
|
|
|
|
2024-02-07 00:14:29 -05:00
|
|
|
if (String(object).startsWith(nconf.get('url'))) {
|
|
|
|
|
const { type, id } = await activitypub.helpers.resolveLocalId(object);
|
|
|
|
|
if (type !== 'post' || !(await posts.exists(id))) {
|
|
|
|
|
throw new Error('[[error:activitypub.invalid-id]]');
|
|
|
|
|
}
|
2024-02-06 14:57:44 -05:00
|
|
|
|
2024-02-07 12:28:27 -05:00
|
|
|
pid = id;
|
|
|
|
|
tid = await posts.getPostField(id, 'tid');
|
2024-02-29 16:09:57 -05:00
|
|
|
|
|
|
|
|
socketHelpers.sendNotificationToPostOwner(pid, actor, 'announce', 'notifications:activitypub.announce');
|
2024-02-07 00:14:29 -05:00
|
|
|
} else {
|
2024-02-07 12:28:27 -05:00
|
|
|
pid = object;
|
2024-02-21 10:58:20 -05:00
|
|
|
pid = await activitypub.resolveId(0, pid); // in case wrong id is passed-in; unlikely, but still.
|
|
|
|
|
if (!pid) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tid = await activitypub.notes.assertTopic(0, pid);
|
2024-02-12 15:25:49 -05:00
|
|
|
if (!tid) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-07 00:14:29 -05:00
|
|
|
await topics.updateLastPostTime(tid, timestamp);
|
2024-02-12 16:23:21 -05:00
|
|
|
await activitypub.notes.updateLocalRecipients(pid, { to, cc });
|
|
|
|
|
await activitypub.notes.syncUserInboxes(tid);
|
2024-02-07 00:14:29 -05:00
|
|
|
}
|
2024-02-07 12:28:27 -05:00
|
|
|
|
2024-02-07 12:50:26 -05:00
|
|
|
winston.info(`[activitypub/inbox/announce] Parsing id ${pid}`);
|
|
|
|
|
|
2024-02-07 12:28:27 -05:00
|
|
|
// No double-announce allowed
|
|
|
|
|
const existing = await topics.events.find(tid, {
|
|
|
|
|
type: 'announce',
|
|
|
|
|
uid: actor,
|
|
|
|
|
pid,
|
|
|
|
|
});
|
|
|
|
|
if (existing.length) {
|
|
|
|
|
await topics.events.purge(tid, existing);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await topics.events.log(tid, {
|
|
|
|
|
type: 'announce',
|
|
|
|
|
uid: actor,
|
2024-02-07 14:29:47 -05:00
|
|
|
href: utils.isNumber(pid) ? `/post/${pid}` : pid,
|
2024-02-07 12:28:27 -05:00
|
|
|
pid,
|
|
|
|
|
timestamp,
|
|
|
|
|
});
|
2024-02-06 14:57:44 -05:00
|
|
|
};
|
|
|
|
|
|
2023-12-22 13:35:09 -05:00
|
|
|
inbox.follow = async (req) => {
|
2023-06-28 14:59:39 -04:00
|
|
|
// Sanity checks
|
2024-02-05 16:57:17 -05:00
|
|
|
const { type, id } = await helpers.resolveLocalId(req.body.object);
|
|
|
|
|
if (!['category', 'user'].includes(type)) {
|
|
|
|
|
throw new Error('[[error:activitypub.invalid-id]]');
|
2023-06-28 14:59:39 -04:00
|
|
|
}
|
|
|
|
|
|
2024-01-26 15:10:35 -05:00
|
|
|
const assertion = await activitypub.actors.assert(req.body.actor);
|
|
|
|
|
if (!assertion) {
|
|
|
|
|
throw new Error('[[error:activitypub.invalid-id]]');
|
2024-01-05 22:45:33 -05:00
|
|
|
}
|
|
|
|
|
|
2024-02-05 16:57:17 -05:00
|
|
|
if (type === 'user') {
|
|
|
|
|
const exists = await user.exists(id);
|
|
|
|
|
if (!exists) {
|
|
|
|
|
throw new Error('[[error:invalid-uid]]');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const isFollowed = await inbox.isFollowed(req.body.actor, id);
|
|
|
|
|
if (isFollowed) {
|
|
|
|
|
// No additional parsing required
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const now = Date.now();
|
|
|
|
|
await db.sortedSetAdd(`followersRemote:${id}`, now, req.body.actor);
|
|
|
|
|
|
|
|
|
|
const followerRemoteCount = await db.sortedSetCard(`followersRemote:${id}`);
|
|
|
|
|
await user.setUserField(id, 'followerRemoteCount', followerRemoteCount);
|
|
|
|
|
|
2024-02-29 16:09:57 -05:00
|
|
|
user.onFollow(req.body.actor, id);
|
|
|
|
|
activitypub.send('uid', id, req.body.actor, {
|
2024-02-05 16:57:17 -05:00
|
|
|
type: 'Accept',
|
|
|
|
|
object: {
|
|
|
|
|
type: 'Follow',
|
|
|
|
|
actor: req.body.actor,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
} else if (type === 'category') {
|
2024-02-26 11:39:32 -05:00
|
|
|
const [exists, allowed] = await Promise.all([
|
|
|
|
|
categories.exists(id),
|
|
|
|
|
privileges.categories.can('read', id, 'activitypub._constants.uid'),
|
|
|
|
|
]);
|
2024-02-05 16:57:17 -05:00
|
|
|
if (!exists) {
|
|
|
|
|
throw new Error('[[error:invalid-cid]]');
|
|
|
|
|
}
|
2024-02-26 11:39:32 -05:00
|
|
|
if (!allowed) {
|
|
|
|
|
return activitypub.send('uid', 0, req.body.actor, {
|
|
|
|
|
type: 'Reject',
|
|
|
|
|
object: {
|
|
|
|
|
type: 'Follow',
|
|
|
|
|
actor: req.body.actor,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
2024-02-05 16:57:17 -05:00
|
|
|
|
|
|
|
|
const watchState = await categories.getWatchState([id], req.body.actor);
|
2024-02-06 10:40:46 -05:00
|
|
|
if (watchState[0] !== categories.watchStates.tracking) {
|
|
|
|
|
await user.setCategoryWatchState(req.body.actor, id, categories.watchStates.tracking);
|
2024-02-05 16:57:17 -05:00
|
|
|
}
|
|
|
|
|
|
2024-02-26 11:39:32 -05:00
|
|
|
activitypub.send('cid', id, req.body.actor, {
|
2024-02-05 16:57:17 -05:00
|
|
|
type: 'Accept',
|
|
|
|
|
object: {
|
|
|
|
|
type: 'Follow',
|
|
|
|
|
actor: req.body.actor,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
2023-12-22 13:35:09 -05:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
inbox.isFollowed = async (actorId, uid) => {
|
|
|
|
|
if (actorId.indexOf('@') === -1 || parseInt(uid, 10) <= 0) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
return await db.isSortedSetMember(`followersRemote:${uid}`, actorId);
|
|
|
|
|
};
|
2023-12-13 13:15:03 -05:00
|
|
|
|
|
|
|
|
inbox.accept = async (req) => {
|
2024-01-26 15:10:35 -05:00
|
|
|
const { actor, object } = req.body;
|
2023-12-13 13:15:03 -05:00
|
|
|
const { type } = object;
|
|
|
|
|
|
2024-02-05 16:57:17 -05:00
|
|
|
const { type: localType, id: uid } = await helpers.resolveLocalId(object.actor);
|
|
|
|
|
if (localType !== 'user' || !uid) {
|
2024-01-05 22:45:33 -05:00
|
|
|
throw new Error('[[error:invalid-uid]]');
|
|
|
|
|
}
|
2024-01-03 13:54:17 -05:00
|
|
|
|
2024-01-26 15:10:35 -05:00
|
|
|
const assertion = await activitypub.actors.assert(actor);
|
|
|
|
|
if (!assertion) {
|
|
|
|
|
throw new Error('[[error:activitypub.invalid-id]]');
|
|
|
|
|
}
|
2023-12-13 13:15:03 -05:00
|
|
|
|
2024-01-05 22:45:33 -05:00
|
|
|
if (type === 'Follow') {
|
2023-12-13 13:15:03 -05:00
|
|
|
const now = Date.now();
|
2024-02-06 10:40:46 -05:00
|
|
|
await db.sortedSetAdd(`followingRemote:${uid}`, now, actor);
|
2024-02-12 14:32:55 -05:00
|
|
|
await db.sortedSetAdd(`followersRemote:${actor}`, now, uid); // for followers backreference
|
2024-02-06 10:40:46 -05:00
|
|
|
const followingRemoteCount = await db.sortedSetCard(`followingRemote:${uid}`);
|
|
|
|
|
await user.setUserField(uid, 'followingRemoteCount', followingRemoteCount);
|
2023-12-13 13:15:03 -05:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
inbox.undo = async (req) => {
|
2024-02-05 16:57:17 -05:00
|
|
|
// todo: "actor" in this case should be the one in object, no?
|
2024-01-26 15:10:35 -05:00
|
|
|
const { actor, object } = req.body;
|
2023-12-13 13:15:03 -05:00
|
|
|
const { type } = object;
|
|
|
|
|
|
2024-02-06 14:57:44 -05:00
|
|
|
if (actor !== object.actor) {
|
|
|
|
|
throw new Error('[[error:activitypub.actor-mismatch]]');
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-26 15:10:35 -05:00
|
|
|
const assertion = await activitypub.actors.assert(actor);
|
|
|
|
|
if (!assertion) {
|
|
|
|
|
throw new Error('[[error:activitypub.invalid-id]]');
|
|
|
|
|
}
|
2023-12-13 13:15:03 -05:00
|
|
|
|
2024-02-20 13:43:45 -05:00
|
|
|
let { type: localType, id } = await helpers.resolveLocalId(object.object);
|
2024-02-05 16:57:17 -05:00
|
|
|
|
2024-02-20 13:43:45 -05:00
|
|
|
winston.info(`[activitypub/inbox/undo] ${type} ${localType && id ? `${localType} ${id}` : object.object} via ${actor}`);
|
2024-02-07 12:50:26 -05:00
|
|
|
|
2024-02-01 15:59:29 -05:00
|
|
|
switch (type) {
|
|
|
|
|
case 'Follow': {
|
2024-02-05 16:57:17 -05:00
|
|
|
switch (localType) {
|
|
|
|
|
case 'user': {
|
|
|
|
|
const exists = await user.exists(id);
|
|
|
|
|
if (!exists) {
|
|
|
|
|
throw new Error('[[error:invalid-uid]]');
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-06 10:40:46 -05:00
|
|
|
await db.sortedSetRemove(`followersRemote:${id}`, actor);
|
|
|
|
|
const followerRemoteCount = await db.sortedSetCard(`followerRemote:${id}`);
|
|
|
|
|
await user.setUserField(id, 'followerRemoteCount', followerRemoteCount);
|
2024-02-29 16:09:57 -05:00
|
|
|
notifications.rescind(`follow:${id}:uid:${actor}`);
|
2024-02-05 16:57:17 -05:00
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case 'category': {
|
|
|
|
|
const exists = await categories.exists(id);
|
|
|
|
|
if (!exists) {
|
|
|
|
|
throw new Error('[[error:invalid-cid]]');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await user.setCategoryWatchState(actor, id, categories.watchStates.notwatching);
|
|
|
|
|
break;
|
|
|
|
|
}
|
2024-02-01 15:59:29 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case 'Like': {
|
2024-02-05 16:57:17 -05:00
|
|
|
const exists = await posts.exists(id);
|
|
|
|
|
if (localType !== 'post' || !exists) {
|
|
|
|
|
throw new Error('[[error:invalid-pid]]');
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-26 11:39:32 -05:00
|
|
|
const allowed = await privileges.posts.can('posts:upvote', id, activitypub._constants.uid);
|
|
|
|
|
if (!allowed) {
|
|
|
|
|
winston.info(`[activitypub/inbox.like] ${id} not allowed to be upvoted.`);
|
|
|
|
|
activitypub.send('uid', 0, actor, {
|
|
|
|
|
type: 'Reject',
|
|
|
|
|
object,
|
|
|
|
|
});
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-05 16:57:17 -05:00
|
|
|
await posts.unvote(id, actor);
|
2024-02-29 16:09:57 -05:00
|
|
|
notifications.rescind(`upvote:post:${id}:uid:${actor}`);
|
2024-02-01 15:59:29 -05:00
|
|
|
break;
|
|
|
|
|
}
|
2024-02-06 14:57:44 -05:00
|
|
|
|
|
|
|
|
case 'Announce': {
|
2024-02-20 13:43:45 -05:00
|
|
|
id = id || object.object; // remote announces
|
2024-02-06 14:57:44 -05:00
|
|
|
const exists = await posts.exists(id);
|
2024-02-20 13:43:45 -05:00
|
|
|
if (!exists) {
|
|
|
|
|
winston.verbose(`[activitypub/inbox/undo] Attempted to undo announce of ${id} but couldn't find it, so doing nothing.`);
|
2024-02-06 14:57:44 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const tid = await posts.getPostField(id, 'tid');
|
|
|
|
|
const existing = await topics.events.find(tid, {
|
|
|
|
|
type: 'announce',
|
|
|
|
|
uid: actor,
|
|
|
|
|
pid: id,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (existing.length) {
|
|
|
|
|
await topics.events.purge(tid, existing);
|
|
|
|
|
}
|
2024-02-29 16:09:57 -05:00
|
|
|
|
|
|
|
|
notifications.rescind(`announce:post:${id}:uid:${actor}`);
|
2024-02-06 14:57:44 -05:00
|
|
|
}
|
2023-12-13 13:15:03 -05:00
|
|
|
}
|
|
|
|
|
};
|