WIP: Import messages sent to local Actor inboxes to their private mail

This commit is contained in:
Bryan Ashby 2023-01-24 21:40:12 -07:00
parent 5b69cdb516
commit 1aa56fbaa7
No known key found for this signature in database
GPG Key ID: C2C1B501E4EFD994
8 changed files with 134 additions and 36 deletions

View File

@ -3,11 +3,11 @@ const ActivityPubObject = require('./object');
const apDb = require('../database').dbs.activitypub; const apDb = require('../database').dbs.activitypub;
const { getISOTimestampString } = require('../database'); const { getISOTimestampString } = require('../database');
const { Errors } = require('../enig_error.js'); const { Errors } = require('../enig_error.js');
const { PublicCollectionId: APPublicCollectionId } = require('./const');
// deps // deps
const { isString, get, isObject } = require('lodash'); const { isString, get, isObject } = require('lodash');
const APPublicCollectionId = 'https://www.w3.org/ns/activitystreams#Public';
const APPublicOwningUserId = 0; const APPublicOwningUserId = 0;
module.exports = class Collection extends ActivityPubObject { 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) { static addPublicInboxItem(inboxItem, cb) {
return Collection.addToCollection( return Collection.addToCollection(
'publicInbox', 'publicInbox',

View File

@ -1,4 +1,5 @@
exports.ActivityStreamsContext = 'https://www.w3.org/ns/activitystreams'; exports.ActivityStreamsContext = 'https://www.w3.org/ns/activitystreams';
exports.PublicCollectionId = 'https://www.w3.org/ns/activitystreams#Public';
const WellKnownActivity = { const WellKnownActivity = {
Create: 'Create', Create: 'Create',

View File

@ -3,7 +3,7 @@ const ActivityPubObject = require('./object');
const { Errors } = require('../enig_error'); const { Errors } = require('../enig_error');
const { getISOTimestampString } = require('../database'); const { getISOTimestampString } = require('../database');
const User = require('../user'); const User = require('../user');
const { messageBodyToHtml } = require('./util'); const { messageBodyToHtml, htmlToMessageBody } = require('./util');
// deps // deps
const { v5: UUIDv5 } = require('uuid'); const { v5: UUIDv5 } = require('uuid');
@ -11,6 +11,7 @@ const Actor = require('./actor');
const moment = require('moment'); const moment = require('moment');
const Collection = require('./collection'); const Collection = require('./collection');
const async = require('async'); const async = require('async');
const { isString, isObject } = require('lodash');
const APMessageIdNamespace = '307bc7b3-3735-4573-9a20-e3f9eaac29c5'; const APMessageIdNamespace = '307bc7b3-3735-4573-9a20-e3f9eaac29c5';
@ -90,7 +91,7 @@ module.exports = class Note extends ActivityPubObject {
audience: [message.isPrivate() ? 'as:Private' : 'as:Public'], audience: [message.isPrivate() ? 'as:Private' : 'as:Public'],
// :TODO: inReplyto if this is a reply; we need this store in message meta. // :TODO: inReplyto if this is a reply; we need this store in message meta.
summary: message.subject,
content: messageBodyToHtml(message.message.trim()), 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 // stable ID based on Note ID
const message = new Message({ const message = new Message({
uuid: UUIDv5(this.id, APMessageIdNamespace), 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) => { Actor.fromId(this.attributedTo, false, (err, attributedToActor) => {
if (err) { if (err) {
// :TODO: Log me // :TODO: Log me
message.toUserName = this.attributedTo; // have some sort of value =/ message.fromUserName = this.attributedTo; // have some sort of value =/
} else { } else {
message.toUserName = message.fromUserName =
attributedToActor.preferredUsername || this.attributedTo; 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.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 { try {
message.modTimestamp = moment(this.published); message.modTimestamp = moment(this.published);
@ -130,21 +146,17 @@ module.exports = class Note extends ActivityPubObject {
message.modTimestamp = moment(); message.modTimestamp = moment();
} }
// :TODO: areaTag
// :TODO: replyToMsgId from 'inReplyTo' // :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.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); return cb(null, message);
}); });

View File

@ -11,6 +11,7 @@ const waterfall = require('async/waterfall');
const fs = require('graceful-fs'); const fs = require('graceful-fs');
const paths = require('path'); const paths = require('path');
const moment = require('moment'); const moment = require('moment');
const { striptags } = require('striptags');
exports.ActivityStreamsContext = 'https://www.w3.org/ns/activitystreams'; exports.ActivityStreamsContext = 'https://www.w3.org/ns/activitystreams';
exports.isValidLink = isValidLink; exports.isValidLink = isValidLink;
@ -22,6 +23,7 @@ exports.userFromAccount = userFromAccount;
exports.accountFromSelfUrl = accountFromSelfUrl; exports.accountFromSelfUrl = accountFromSelfUrl;
exports.getUserProfileTemplatedBody = getUserProfileTemplatedBody; exports.getUserProfileTemplatedBody = getUserProfileTemplatedBody;
exports.messageBodyToHtml = messageBodyToHtml; exports.messageBodyToHtml = messageBodyToHtml;
exports.htmlToMessageBody = htmlToMessageBody;
// :TODO: more info in default // :TODO: more info in default
// this profile template is the *default* for both WebFinger // this profile template is the *default* for both WebFinger
@ -175,3 +177,7 @@ function getUserProfileTemplatedBody(
function messageBodyToHtml(body) { function messageBodyToHtml(body) {
return `<p>${body.replace(/\r?\n/g, '<br>')}</p>`; return `<p>${body.replace(/\r?\n/g, '<br>')}</p>`;
} }
function htmlToMessageBody(html) {
return striptags(html);
}

View File

@ -11,6 +11,7 @@ const ActivityPubSettings = require('../../../activitypub/settings');
const Actor = require('../../../activitypub/actor'); const Actor = require('../../../activitypub/actor');
const Collection = require('../../../activitypub/collection'); const Collection = require('../../../activitypub/collection');
const EnigAssert = require('../../../enigma_assert'); const EnigAssert = require('../../../enigma_assert');
const Message = require('../../../message');
// deps // deps
const _ = require('lodash'); const _ = require('lodash');
@ -18,6 +19,7 @@ const enigma_assert = require('../../../enigma_assert');
const httpSignature = require('http-signature'); const httpSignature = require('http-signature');
const async = require('async'); const async = require('async');
const Note = require('../../../activitypub/note'); const Note = require('../../../activitypub/note');
const User = require('../../../user');
exports.moduleInfo = { exports.moduleInfo = {
name: 'ActivityPub', name: 'ActivityPub',
@ -262,12 +264,6 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
} }
_sharedInboxCreateActivity(req, resp, activity) { _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; let toActors = activity.to;
if (!Array.isArray(toActors)) { if (!Array.isArray(toActors)) {
toActors = [toActors]; toActors = [toActors];
@ -288,22 +284,36 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
} }
_deliverSharedInboxNote(req, resp, toActors, activity) { _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( async.forEach(
toActors, toActors,
(actor, nextActor) => { (actorId, nextActor) => {
if (Collection.PublicCollectionId === actor) { if (Collection.PublicCollectionId === actorId) {
// Deliver to inbox for "everyone": // Deliver to inbox for "everyone":
// - Add to 'sharedInbox' collection // - Add to 'sharedInbox' collection
// //
Collection.addPublicInboxItem(activity.object, err => { Collection.addPublicInboxItem(note, err => {
if (err) {
return nextActor(err); return nextActor(err);
}
return nextActor(null);
}); });
} else { } else {
nextActor(null); this._deliverInboxNoteToLocalActor(
req,
resp,
actorId,
note,
nextActor
);
} }
}, },
err => { 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) { _getCollectionHandler(name, req, resp, signature) {
EnigAssert(signature, 'Missing signature!'); EnigAssert(signature, 'Missing signature!');

View File

@ -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) { static getUserIdAndNameByRealName(realName, cb) {
userDb.get( userDb.get(
`SELECT id, user_name `SELECT id, user_name

View File

@ -63,6 +63,7 @@
"sqlite3": "5.0.11", "sqlite3": "5.0.11",
"sqlite3-trans": "1.3.0", "sqlite3-trans": "1.3.0",
"ssh2": "1.11.0", "ssh2": "1.11.0",
"striptags": "^4.0.0-alpha.4",
"systeminformation": "5.12.3", "systeminformation": "5.12.3",
"telnet-socket": "0.2.4", "telnet-socket": "0.2.4",
"temptmp": "^1.1.0", "temptmp": "^1.1.0",

View File

@ -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" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
integrity sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ== 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: supports-color@^7.1.0:
version "7.1.0" version "7.1.0"
resolved "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz" resolved "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz"