From 2ab5ea39a6238d4c608938c78a0c8c8325148ad4 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Mon, 7 Oct 2024 14:09:34 -0400 Subject: [PATCH] feat: federating out chat messages re #12834 --- src/activitypub/helpers.js | 2 +- src/activitypub/mocks.js | 66 ++++++++++++++++++++++++++- src/api/activitypub.js | 22 +++++++-- src/controllers/activitypub/actors.js | 14 +++++- src/messaging/notifications.js | 10 ++-- src/routes/activitypub.js | 2 + 6 files changed, 104 insertions(+), 12 deletions(-) diff --git a/src/activitypub/helpers.js b/src/activitypub/helpers.js index 0cf0ff87de..9588353623 100644 --- a/src/activitypub/helpers.js +++ b/src/activitypub/helpers.js @@ -264,7 +264,7 @@ Helpers.resolveObjects = async (ids) => { if (!post) { throw new Error('[[error:activitypub.invalid-id]]'); } - return activitypub.mocks.note(post); + return activitypub.mocks.notes.public(post); } case 'category': { if (!await categories.exists(resolvedId)) { diff --git a/src/activitypub/mocks.js b/src/activitypub/mocks.js index 4e286bf0ff..f113548af7 100644 --- a/src/activitypub/mocks.js +++ b/src/activitypub/mocks.js @@ -10,6 +10,7 @@ const user = require('../user'); const categories = require('../categories'); const posts = require('../posts'); const topics = require('../topics'); +const messaging = require('../messaging'); const plugins = require('../plugins'); const slugify = require('../slugify'); const translator = require('../translator'); @@ -271,7 +272,9 @@ Mocks.actors.category = async (cid) => { }; }; -Mocks.note = async (post) => { +Mocks.notes = {}; + +Mocks.notes.public = async (post) => { const id = `${nconf.get('url')}/post/${post.pid}`; // Return a tombstone for a deleted post @@ -434,7 +437,6 @@ Mocks.note = async (post) => { attributedTo: `${nconf.get('url')}/uid/${post.user.uid}`, context: `${nconf.get('url')}/topic/${post.topic.tid}`, audience: `${nconf.get('url')}/category/${post.category.cid}`, - sensitive: false, // todo summary: null, name, content: post.content, @@ -447,6 +449,66 @@ Mocks.note = async (post) => { return object; }; +Mocks.notes.private = async ({ messageObj }) => { + // todo: deleted messages + let uids = await messaging.getUidsInRoom(messageObj.roomId, 0, -1); + const remoteUids = uids.filter(uid => !utils.isNumber(uid)); + uids = uids.map(uid => (utils.isNumber(uid) ? `${nconf.get('url')}/uid/${uid}` : uid)); + const id = `${nconf.get('url')}/message/${messageObj.mid}`; + const to = new Set(uids); + const published = messageObj.timestampISO; + const updated = messageObj.edited ? messageObj.editedISO : undefined; + const content = await messaging.parse(messageObj.content, messageObj.fromuid, 0, messageObj.roomId, false); + + let source; + const markdownEnabled = await plugins.isActive('nodebb-plugin-markdown'); + if (markdownEnabled) { + source = { + content: messageObj.content, + mediaType: 'text/markdown', + }; + } + + const mentions = await user.getUsersFields(remoteUids, ['uid', 'userslug']); + const tag = []; + tag.push(...mentions.map(({ uid, userslug }) => ({ + type: 'Mention', + href: uid, + name: userslug, + }))); + + let inReplyTo; + if (messageObj.toMid) { + inReplyTo = utils.isNumber(messageObj.toMid) ? + `${nconf.get('url')}/api/v3/chats/${messageObj.roomId}/messages/${messageObj.toMid}` : + messageObj.toMid; + } + + const object = { + '@context': 'https://www.w3.org/ns/activitystreams', + id, + type: 'Note', + to: Array.from(to), + cc: [], + inReplyTo, + published, + updated, + url: id, + attributedTo: `${nconf.get('url')}/uid/${messageObj.fromuid}`, + // context: `${nconf.get('url')}/topic/${post.topic.tid}`, + // audience: `${nconf.get('url')}/category/${post.category.cid}`, + summary: null, + // name, + content: content, + source, + tag, + // attachment: [], // todo + // replies: `${id}/replies`, // todo + }; + + return object; +}; + Mocks.tombstone = async properties => ({ '@context': 'https://www.w3.org/ns/activitystreams', type: 'Tombstone', diff --git a/src/api/activitypub.js b/src/api/activitypub.js index 49bb168b44..9ab9d89390 100644 --- a/src/api/activitypub.js +++ b/src/api/activitypub.js @@ -18,6 +18,7 @@ const privileges = require('../privileges'); const activitypub = require('../activitypub'); const posts = require('../posts'); const topics = require('../topics'); +const messaging = require('../messaging'); const utils = require('../utils'); const activitypubApi = module.exports; @@ -180,7 +181,7 @@ activitypubApi.create.note = enabledCheck(async (caller, { pid, post }) => { return; } - const object = await activitypub.mocks.note(post); + const object = await activitypub.mocks.notes.public(post); const { to, cc, targets } = await buildRecipients(object, { pid, uid: post.user.uid }); const { cid } = post.category; const followers = await activitypub.notes.getCategoryFollowers(cid); @@ -211,6 +212,21 @@ activitypubApi.create.note = enabledCheck(async (caller, { pid, post }) => { } }); +activitypubApi.create.privateNote = enabledCheck(async (caller, { mid, messageObj }) => { + if (!messageObj) { + messageObj = await messaging.getMessageFields(mid, []); + if (!messageObj) { + throw new Error('[[error:invalid-data]]'); + } + } + const { roomId } = messageObj; + let targets = await messaging.getUidsInRoom(roomId, 0, -1); + targets = targets.filter(uid => !utils.isNumber(uid)); // remote uids only + + const payload = await activitypub.mocks.notes.private({ messageObj }); + await activitypub.send('uid', messageObj.fromuid, targets, payload); +}); + activitypubApi.update = {}; activitypubApi.update.profile = enabledCheck(async (caller, { uid }) => { @@ -249,7 +265,7 @@ activitypubApi.update.note = enabledCheck(async (caller, { post }) => { return; } - const object = await activitypub.mocks.note(post); + const object = await activitypub.mocks.notes.public(post); const { to, cc, targets } = await buildRecipients(object, { pid: post.pid, uid: post.user.uid }); const allowed = await privileges.posts.can('topics:read', post.pid, activitypub._constants.uid); @@ -281,7 +297,7 @@ activitypubApi.delete.note = enabledCheck(async (caller, { pid }) => { const id = `${nconf.get('url')}/post/${pid}`; const post = (await posts.getPostSummaryByPids([pid], caller.uid, { stripTags: false })).pop(); - const object = await activitypub.mocks.note(post); + const object = await activitypub.mocks.notes.public(post); const { to, cc, targets } = await buildRecipients(object, { pid, uid: post.user.uid }); const allowed = await privileges.posts.can('topics:read', pid, activitypub._constants.uid); diff --git a/src/controllers/activitypub/actors.js b/src/controllers/activitypub/actors.js index c3ccb72fc4..2ffd0f5655 100644 --- a/src/controllers/activitypub/actors.js +++ b/src/controllers/activitypub/actors.js @@ -9,6 +9,7 @@ const privileges = require('../../privileges'); const posts = require('../../posts'); const topics = require('../../topics'); const categories = require('../../categories'); +const messaging = require('../../messaging'); const activitypub = require('../../activitypub'); const utils = require('../../utils'); @@ -75,7 +76,7 @@ Actors.note = async function (req, res) { return res.sendStatus(404); } - const payload = await activitypub.mocks.note(post); + const payload = await activitypub.mocks.notes.public(post); res.status(200).json(payload); }; @@ -184,3 +185,14 @@ Actors.category = async function (req, res, next) { const payload = await activitypub.mocks.actors.category(req.params.cid); res.status(200).json(payload); }; + +Actors.message = async function (req, res, next) { + // Handle requests for remote content + if (!utils.isNumber(req.params.mid)) { + return res.set('Location', req.params.mid).sendStatus(308); + } + + const messageObj = await messaging.getMessageFields(req.params.mid, []); + const payload = await activitypub.mocks.notes.private({ messageObj }); + res.status(200).json(payload); +}; diff --git a/src/messaging/notifications.js b/src/messaging/notifications.js index 506a280d39..87b9a78dc4 100644 --- a/src/messaging/notifications.js +++ b/src/messaging/notifications.js @@ -8,6 +8,7 @@ const db = require('../database'); const notifications = require('../notifications'); const user = require('../user'); const io = require('../socket.io'); +const api = require('../api'); const plugins = require('../plugins'); const utils = require('../utils'); @@ -79,8 +80,10 @@ module.exports = function (Messaging) { } try { - await sendNotification(fromUid, roomId, messageObj); - // await federate(fromUid, roomId, messageObj); + await Promise.all([ + sendNotification(fromUid, roomId, messageObj), + !isPublic ? api.activitypub.create.privateNote({ uid: fromUid }, { messageObj }) : null, + ]); } catch (err) { winston.error(`[messaging/notifications] Unabled to send notification\n${err.stack}`); } @@ -142,7 +145,4 @@ module.exports = function (Messaging) { await notifications.push(notification, uidsToNotify); } } - - // async function federate(fromUid, roomId, messageObj) { - // } }; diff --git a/src/routes/activitypub.js b/src/routes/activitypub.js index d60365dda2..9a378b41b1 100644 --- a/src/routes/activitypub.js +++ b/src/routes/activitypub.js @@ -45,4 +45,6 @@ module.exports = function (app, middleware, controllers) { app.post('/category/:cid/inbox', [...inboxMiddlewares, middleware.assert.category, ...inboxMiddlewares], controllers.activitypub.postInbox); app.get('/category/:cid/outbox', [...middlewares, middleware.assert.category], controllers.activitypub.getCategoryOutbox); app.post('/category/:cid/outbox', [...middlewares, middleware.assert.category], controllers.activitypub.postOutbox); + + app.get('/message/:mid', [...middlewares, middleware.assert.message], controllers.activitypub.actors.message); };