From 1aa56fbaa7ff4013affa7b368ff0493856afa3a1 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 24 Jan 2023 21:40:12 -0700 Subject: [PATCH] WIP: Import messages sent to local Actor inboxes to their private mail --- core/activitypub/collection.js | 13 ++- core/activitypub/const.js | 1 + core/activitypub/note.js | 52 +++++++----- core/activitypub/util.js | 6 ++ .../content/web_handlers/activitypub.js | 83 +++++++++++++++---- core/user.js | 9 ++ package.json | 1 + yarn.lock | 5 ++ 8 files changed, 134 insertions(+), 36 deletions(-) diff --git a/core/activitypub/collection.js b/core/activitypub/collection.js index 3f719208..43da2535 100644 --- a/core/activitypub/collection.js +++ b/core/activitypub/collection.js @@ -3,11 +3,11 @@ const ActivityPubObject = require('./object'); const apDb = require('../database').dbs.activitypub; const { getISOTimestampString } = require('../database'); const { Errors } = require('../enig_error.js'); +const { PublicCollectionId: APPublicCollectionId } = require('./const'); // deps const { isString, get, isObject } = require('lodash'); -const APPublicCollectionId = 'https://www.w3.org/ns/activitystreams#Public'; const APPublicOwningUserId = 0; module.exports = class Collection extends ActivityPubObject { @@ -88,6 +88,17 @@ module.exports = class Collection extends ActivityPubObject { ); } + static addInboxItem(inboxItem, owningUser, cb) { + return Collection.addToCollection( + 'inbox', + owningUser, + inboxItem.id, + inboxItem, + true, + cb + ); + } + static addPublicInboxItem(inboxItem, cb) { return Collection.addToCollection( 'publicInbox', diff --git a/core/activitypub/const.js b/core/activitypub/const.js index b5602770..61e005f2 100644 --- a/core/activitypub/const.js +++ b/core/activitypub/const.js @@ -1,4 +1,5 @@ exports.ActivityStreamsContext = 'https://www.w3.org/ns/activitystreams'; +exports.PublicCollectionId = 'https://www.w3.org/ns/activitystreams#Public'; const WellKnownActivity = { Create: 'Create', diff --git a/core/activitypub/note.js b/core/activitypub/note.js index 29e0b96f..179d2e9f 100644 --- a/core/activitypub/note.js +++ b/core/activitypub/note.js @@ -3,7 +3,7 @@ const ActivityPubObject = require('./object'); const { Errors } = require('../enig_error'); const { getISOTimestampString } = require('../database'); const User = require('../user'); -const { messageBodyToHtml } = require('./util'); +const { messageBodyToHtml, htmlToMessageBody } = require('./util'); // deps const { v5: UUIDv5 } = require('uuid'); @@ -11,6 +11,7 @@ const Actor = require('./actor'); const moment = require('moment'); const Collection = require('./collection'); const async = require('async'); +const { isString, isObject } = require('lodash'); const APMessageIdNamespace = '307bc7b3-3735-4573-9a20-e3f9eaac29c5'; @@ -90,7 +91,7 @@ module.exports = class Note extends ActivityPubObject { audience: [message.isPrivate() ? 'as:Private' : 'as:Public'], // :TODO: inReplyto if this is a reply; we need this store in message meta. - + summary: message.subject, content: messageBodyToHtml(message.message.trim()), }; @@ -104,24 +105,39 @@ module.exports = class Note extends ActivityPubObject { ); } - toMessage(cb) { + toMessage(options, cb) { + if (!isObject(options.toUser) || !isString(options.areaTag)) { + return cb(Errors.MissingParam('Missing one or more required options!')); + } + // stable ID based on Note ID const message = new Message({ uuid: UUIDv5(this.id, APMessageIdNamespace), }); - // Fetch the remote actor + // Fetch the remote actor info to get their user info Actor.fromId(this.attributedTo, false, (err, attributedToActor) => { if (err) { // :TODO: Log me - message.toUserName = this.attributedTo; // have some sort of value =/ + message.fromUserName = this.attributedTo; // have some sort of value =/ } else { - message.toUserName = + message.fromUserName = attributedToActor.preferredUsername || this.attributedTo; } + // + // Note's can be addressed to 1:N users, but a Message is a 1:1 + // relationship. This method requires the mapping up front via options + // + (message.toUserName = options.toUser.username), + (message.meta.System[Message.SystemMetaNames.LocalToUserID] = + options.toUser.userId); + message.areaTag = options.areaTag || Message.WellKnownAreaTags.Private; + message.subject = this.summary || '-ActivityPub-'; - message.message = this.content; // :TODO: HTML to suitable format, or even strip + + // :TODO: it would be better to do some basic HTML to ANSI or pipe codes perhaps + message.message = htmlToMessageBody(this.content); try { message.modTimestamp = moment(this.published); @@ -130,21 +146,17 @@ module.exports = class Note extends ActivityPubObject { message.modTimestamp = moment(); } - // :TODO: areaTag // :TODO: replyToMsgId from 'inReplyTo' - // :TODO: RemoteFromUser - - message.meta[Message.WellKnownMetaCategories.ActivityPub] = - message.meta[Message.WellKnownMetaCategories.ActivityPub] || {}; - const apMeta = message.meta[Message.WellKnownAreaTags.ActivityPub]; - - apMeta[Message.ActivityPubPropertyNames.ActivityId] = this.id; - if (this.InReplyTo) { - apMeta[Message.ActivityPubPropertyNames.InReplyTo] = this.InReplyTo; - } - message.setRemoteFromUser(this.attributedTo); - message.setExternalFlavor(Message.ExternalFlavor.ActivityPub); + message.setExternalFlavor(Message.AddressFlavor.ActivityPub); + + message.meta.ActivityPub = message.meta.ActivityPub || {}; + message.meta.ActivityPub[Message.ActivityPubPropertyNames.ActivityId] = + this.id; + if (this.InReplyTo) { + message.meta.ActivityPub[Message.ActivityPubPropertyNames.InReplyTo] = + this.InReplyTo; + } return cb(null, message); }); diff --git a/core/activitypub/util.js b/core/activitypub/util.js index a155f1b7..4b8b25e3 100644 --- a/core/activitypub/util.js +++ b/core/activitypub/util.js @@ -11,6 +11,7 @@ const waterfall = require('async/waterfall'); const fs = require('graceful-fs'); const paths = require('path'); const moment = require('moment'); +const { striptags } = require('striptags'); exports.ActivityStreamsContext = 'https://www.w3.org/ns/activitystreams'; exports.isValidLink = isValidLink; @@ -22,6 +23,7 @@ exports.userFromAccount = userFromAccount; exports.accountFromSelfUrl = accountFromSelfUrl; exports.getUserProfileTemplatedBody = getUserProfileTemplatedBody; exports.messageBodyToHtml = messageBodyToHtml; +exports.htmlToMessageBody = htmlToMessageBody; // :TODO: more info in default // this profile template is the *default* for both WebFinger @@ -175,3 +177,7 @@ function getUserProfileTemplatedBody( function messageBodyToHtml(body) { return `

${body.replace(/\r?\n/g, '
')}

`; } + +function htmlToMessageBody(html) { + return striptags(html); +} diff --git a/core/servers/content/web_handlers/activitypub.js b/core/servers/content/web_handlers/activitypub.js index 7fb6fbdf..05e836f3 100644 --- a/core/servers/content/web_handlers/activitypub.js +++ b/core/servers/content/web_handlers/activitypub.js @@ -11,6 +11,7 @@ const ActivityPubSettings = require('../../../activitypub/settings'); const Actor = require('../../../activitypub/actor'); const Collection = require('../../../activitypub/collection'); const EnigAssert = require('../../../enigma_assert'); +const Message = require('../../../message'); // deps const _ = require('lodash'); @@ -18,6 +19,7 @@ const enigma_assert = require('../../../enigma_assert'); const httpSignature = require('http-signature'); const async = require('async'); const Note = require('../../../activitypub/note'); +const User = require('../../../user'); exports.moduleInfo = { name: 'ActivityPub', @@ -262,12 +264,6 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule { } _sharedInboxCreateActivity(req, resp, activity) { - // When an object is being delivered to the originating actor's followers, - // a server MAY reduce the number of receiving actors delivered to by - // identifying all followers which share the same sharedInbox who would - // otherwise be individual recipients and instead deliver objects to said - // sharedInbox. Thus in this scenario, the remote/receiving server participates - // in determining targeting and performing delivery to specific inboxes. let toActors = activity.to; if (!Array.isArray(toActors)) { toActors = [toActors]; @@ -288,22 +284,36 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule { } _deliverSharedInboxNote(req, resp, toActors, activity) { + // When an object is being delivered to the originating actor's followers, + // a server MAY reduce the number of receiving actors delivered to by + // identifying all followers which share the same sharedInbox who would + // otherwise be individual recipients and instead deliver objects to said + // sharedInbox. Thus in this scenario, the remote/receiving server participates + // in determining targeting and performing delivery to specific inboxes. + const note = new Note(activity.object); + if (!note.isValid()) { + // :TODO: Log me + return this.webServer.notImplemented(); + } + async.forEach( toActors, - (actor, nextActor) => { - if (Collection.PublicCollectionId === actor) { + (actorId, nextActor) => { + if (Collection.PublicCollectionId === actorId) { // Deliver to inbox for "everyone": // - Add to 'sharedInbox' collection // - Collection.addPublicInboxItem(activity.object, err => { - if (err) { - return nextActor(err); - } - - return nextActor(null); + Collection.addPublicInboxItem(note, err => { + return nextActor(err); }); } else { - nextActor(null); + this._deliverInboxNoteToLocalActor( + req, + resp, + actorId, + note, + nextActor + ); } }, err => { @@ -317,6 +327,49 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule { ); } + _deliverInboxNoteToLocalActor(req, resp, actorId, note, cb) { + const localUserName = accountFromSelfUrl(actorId); + if (!localUserName) { + this.log.debug({ url: req.url }, 'Could not get username from URL'); + return cb(null); + } + + User.getUserByUsername(localUserName, (err, localUser) => { + if (err) { + this.log.info( + { username: localUserName }, + `No local user account for "${localUserName}"` + ); + return cb(null); + } + + Collection.addInboxItem(note, localUser, err => { + if (err) { + return cb(err); + } + + // + // Import the item to the user's private mailbox + // + const messageOpts = { + // Notes can have 1:N 'to' relationships while a Message is 1:1; + toUser: localUser, + areaTag: Message.WellKnownAreaTags.Private, + }; + + note.toMessage(messageOpts, (err, message) => { + if (err) { + return cb(err); + } + + message.persist(err => { + return cb(err); + }); + }); + }); + }); + } + _getCollectionHandler(name, req, resp, signature) { EnigAssert(signature, 'Missing signature!'); diff --git a/core/user.js b/core/user.js index b2b39e3f..f03b9c69 100644 --- a/core/user.js +++ b/core/user.js @@ -937,6 +937,15 @@ module.exports = class User { ); } + static getUserByUsername(username, cb) { + User.getUserIdAndName(username, (err, userId) => { + if (err) { + return cb(err); + } + return User.getUser(userId, cb); + }); + } + static getUserIdAndNameByRealName(realName, cb) { userDb.get( `SELECT id, user_name diff --git a/package.json b/package.json index 7e4bd4cc..9eae0e51 100644 --- a/package.json +++ b/package.json @@ -63,6 +63,7 @@ "sqlite3": "5.0.11", "sqlite3-trans": "1.3.0", "ssh2": "1.11.0", + "striptags": "^4.0.0-alpha.4", "systeminformation": "5.12.3", "telnet-socket": "0.2.4", "temptmp": "^1.1.0", diff --git a/yarn.lock b/yarn.lock index b7b82df2..14f559c9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2675,6 +2675,11 @@ strip-json-comments@~2.0.1: resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" integrity sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ== +striptags@^4.0.0-alpha.4: + version "4.0.0-alpha.4" + resolved "https://registry.yarnpkg.com/striptags/-/striptags-4.0.0-alpha.4.tgz#824f1ac040f824574316ce87a3663c0c4df9900d" + integrity sha512-/0jWyVWhpg9ciRHfjKYBpMHXct/HrFRfsR2HU77nGPbc8SPcVSIHZlZR/0TG3MyPq2C+HiHuwx8BlbcdI/cNbw== + supports-color@^7.1.0: version "7.1.0" resolved "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz"