Better handling of to/from HTML and BBS message formats, Note handling esp with inReplyTo, etc.
This commit is contained in:
parent
4f632fd8c4
commit
0bd2c3db1c
|
@ -4,6 +4,7 @@ const { Errors } = require('../enig_error');
|
||||||
const { getISOTimestampString } = require('../database');
|
const { getISOTimestampString } = require('../database');
|
||||||
const User = require('../user');
|
const User = require('../user');
|
||||||
const { messageBodyToHtml, htmlToMessageBody } = require('./util');
|
const { messageBodyToHtml, htmlToMessageBody } = require('./util');
|
||||||
|
const { isAnsi } = require('../string_util');
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const { v5: UUIDv5 } = require('uuid');
|
const { v5: UUIDv5 } = require('uuid');
|
||||||
|
@ -11,7 +12,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 { isString, isObject, truncate } = require('lodash');
|
||||||
|
|
||||||
const APMessageIdNamespace = '307bc7b3-3735-4573-9a20-e3f9eaac29c5';
|
const APMessageIdNamespace = '307bc7b3-3735-4573-9a20-e3f9eaac29c5';
|
||||||
const APDefaultSummary = '[ActivityPub]';
|
const APDefaultSummary = '[ActivityPub]';
|
||||||
|
@ -77,26 +78,57 @@ module.exports = class Note extends ActivityPubObject {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
(fromUser, fromActor, remoteActor, callback) => {
|
(fromUser, fromActor, remoteActor, callback) => {
|
||||||
const to = message.isPrivate()
|
if (!message.replyToMsgId) {
|
||||||
? remoteActor.id
|
return callback(null, null, fromUser, fromActor, remoteActor);
|
||||||
: Collection.PublicCollectionId;
|
}
|
||||||
|
|
||||||
// Refs
|
Message.getMetaValuesByMessageId(
|
||||||
// - https://docs.joinmastodon.org/spec/activitypub/#properties-used
|
message.replyToMsgId,
|
||||||
|
Message.WellKnownMetaCategories.ActivityPub,
|
||||||
|
Message.ActivityPubPropertyNames.NoteId,
|
||||||
|
(err, replyToNoteId) => {
|
||||||
|
// (ignore error)
|
||||||
|
return callback(
|
||||||
|
null,
|
||||||
|
replyToNoteId,
|
||||||
|
fromUser,
|
||||||
|
fromActor,
|
||||||
|
remoteActor
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
(replyToNoteId, fromUser, fromActor, remoteActor, callback) => {
|
||||||
|
const to = [
|
||||||
|
message.isPrivate()
|
||||||
|
? remoteActor.id
|
||||||
|
: Collection.PublicCollectionId,
|
||||||
|
];
|
||||||
|
|
||||||
|
const sourceMediaType = isAnsi(message.message)
|
||||||
|
? 'text/x-ansi' // ye ol' https://lists.freedesktop.org/archives/xdg/2006-March/006214.html
|
||||||
|
: 'text/plain';
|
||||||
|
|
||||||
|
// https://docs.joinmastodon.org/spec/activitypub/#properties-used
|
||||||
const obj = {
|
const obj = {
|
||||||
id: ActivityPubObject.makeObjectId(webServer, 'note'),
|
id: ActivityPubObject.makeObjectId(webServer, 'note'),
|
||||||
type: 'Note',
|
type: 'Note',
|
||||||
published: getISOTimestampString(message.modTimestamp),
|
published: getISOTimestampString(message.modTimestamp),
|
||||||
to,
|
to,
|
||||||
attributedTo: fromActor.id,
|
attributedTo: fromActor.id,
|
||||||
audience: [message.isPrivate() ? 'as:Private' : 'as:Public'],
|
|
||||||
|
|
||||||
// :TODO: inReplyto if this is a reply; we need this store in message meta.
|
|
||||||
content: messageBodyToHtml(message.message.trim()),
|
content: messageBodyToHtml(message.message.trim()),
|
||||||
|
source: {
|
||||||
|
content: message.message,
|
||||||
|
mediaType: sourceMediaType,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Filter out replace replacement
|
if (replyToNoteId) {
|
||||||
if (message.subject !== APDefaultSummary) {
|
obj.inReplyTo = replyToNoteId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ignore the subject if it's our default summary value for replies
|
||||||
|
if (message.subject !== `RE: ${APDefaultSummary}`) {
|
||||||
obj.summary = message.subject;
|
obj.summary = message.subject;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -137,10 +169,12 @@ module.exports = class Note extends ActivityPubObject {
|
||||||
options.toUser.userId;
|
options.toUser.userId;
|
||||||
message.areaTag = options.areaTag || Message.WellKnownAreaTags.Private;
|
message.areaTag = options.areaTag || Message.WellKnownAreaTags.Private;
|
||||||
|
|
||||||
message.subject = this.summary || APDefaultSummary;
|
|
||||||
|
|
||||||
// :TODO: it would be better to do some basic HTML to ANSI or pipe codes perhaps
|
// :TODO: it would be better to do some basic HTML to ANSI or pipe codes perhaps
|
||||||
message.message = htmlToMessageBody(this.content);
|
message.message = htmlToMessageBody(this.content);
|
||||||
|
message.subject =
|
||||||
|
this.summary ||
|
||||||
|
truncate(message.message, { length: 32, omission: '...' }) ||
|
||||||
|
APDefaultSummary;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
message.modTimestamp = moment(this.published);
|
message.modTimestamp = moment(this.published);
|
||||||
|
@ -155,10 +189,12 @@ module.exports = class Note extends ActivityPubObject {
|
||||||
|
|
||||||
message.meta.ActivityPub = message.meta.ActivityPub || {};
|
message.meta.ActivityPub = message.meta.ActivityPub || {};
|
||||||
message.meta.ActivityPub[Message.ActivityPubPropertyNames.ActivityId] =
|
message.meta.ActivityPub[Message.ActivityPubPropertyNames.ActivityId] =
|
||||||
this.id;
|
options.activityId || 0;
|
||||||
if (this.InReplyTo) {
|
message.meta.ActivityPub[Message.ActivityPubPropertyNames.NoteId] = this.id;
|
||||||
|
|
||||||
|
if (this.inReplyTo) {
|
||||||
message.meta.ActivityPub[Message.ActivityPubPropertyNames.InReplyTo] =
|
message.meta.ActivityPub[Message.ActivityPubPropertyNames.InReplyTo] =
|
||||||
this.InReplyTo;
|
this.inReplyTo;
|
||||||
}
|
}
|
||||||
|
|
||||||
return cb(null, message);
|
return cb(null, message);
|
||||||
|
|
|
@ -3,6 +3,7 @@ const User = require('../user');
|
||||||
const { Errors, ErrorReasons } = require('../enig_error');
|
const { Errors, ErrorReasons } = require('../enig_error');
|
||||||
const UserProps = require('../user_property');
|
const UserProps = require('../user_property');
|
||||||
const ActivityPubSettings = require('./settings');
|
const ActivityPubSettings = require('./settings');
|
||||||
|
const { stripAnsiControlCodes } = require('../string_util');
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
|
@ -12,6 +13,7 @@ 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');
|
const { striptags } = require('striptags');
|
||||||
|
const { encode, decode } = require('html-entities');
|
||||||
|
|
||||||
exports.ActivityStreamsContext = 'https://www.w3.org/ns/activitystreams';
|
exports.ActivityStreamsContext = 'https://www.w3.org/ns/activitystreams';
|
||||||
exports.isValidLink = isValidLink;
|
exports.isValidLink = isValidLink;
|
||||||
|
@ -173,14 +175,23 @@ function getUserProfileTemplatedBody(
|
||||||
//
|
//
|
||||||
// Apply very basic HTML to a message following
|
// Apply very basic HTML to a message following
|
||||||
// Mastodon's supported tags of 'p', 'br', 'a', and 'span':
|
// Mastodon's supported tags of 'p', 'br', 'a', and 'span':
|
||||||
// https://blog.joinmastodon.org/2018/06/how-to-implement-a-basic-activitypub-server/
|
// - https://docs.joinmastodon.org/spec/activitypub/#sanitization
|
||||||
|
// - https://blog.joinmastodon.org/2018/06/how-to-implement-a-basic-activitypub-server/
|
||||||
//
|
//
|
||||||
|
// :TODO: https://docs.joinmastodon.org/spec/microformats/
|
||||||
function messageBodyToHtml(body) {
|
function messageBodyToHtml(body) {
|
||||||
return `<p>${body.replace(/\r?\n/g, '<br>')}</p>`;
|
body = encode(stripAnsiControlCodes(body), { mode: 'nonAsciiPrintable' }).replace(
|
||||||
|
/\r?\n/g,
|
||||||
|
'<br>'
|
||||||
|
);
|
||||||
|
|
||||||
|
return `<p>${body}</p>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function htmlToMessageBody(html) {
|
function htmlToMessageBody(html) {
|
||||||
return striptags(html);
|
// <br>, </br>, and <br/> -> \r\n
|
||||||
|
html = html.replace(/<\/?br?\/?>/g, '\r\n');
|
||||||
|
return striptags(decode(html));
|
||||||
}
|
}
|
||||||
|
|
||||||
function userNameFromSubject(subject) {
|
function userNameFromSubject(subject) {
|
||||||
|
|
|
@ -117,6 +117,7 @@ const QWKPropertyNames = {
|
||||||
const ActivityPubPropertyNames = {
|
const ActivityPubPropertyNames = {
|
||||||
ActivityId: 'activitypub_activity_id', // Activity ID; FK to AP table entries
|
ActivityId: 'activitypub_activity_id', // Activity ID; FK to AP table entries
|
||||||
InReplyTo: 'activitypub_in_reply_to', // Activity ID from 'inReplyTo' field
|
InReplyTo: 'activitypub_in_reply_to', // Activity ID from 'inReplyTo' field
|
||||||
|
NoteId: 'activitypub_note_id', // Note ID specific to Note Activities
|
||||||
};
|
};
|
||||||
|
|
||||||
// :TODO: this is a ugly hack due to bad variable names - clean it up & just _.camelCase(k)!
|
// :TODO: this is a ugly hack due to bad variable names - clean it up & just _.camelCase(k)!
|
||||||
|
|
|
@ -132,6 +132,16 @@ exports.getModule = class ActivityPubScannerTosser extends MessageScanTossModule
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
(activity, callback) => {
|
||||||
|
return message.persistMetaValue(
|
||||||
|
Message.WellKnownMetaCategories.ActivityPub,
|
||||||
|
Message.ActivityPubPropertyNames.NoteId,
|
||||||
|
activity.object.id,
|
||||||
|
err => {
|
||||||
|
return callback(err, activity);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
],
|
],
|
||||||
(err, activity) => {
|
(err, activity) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
|
|
|
@ -313,6 +313,11 @@ exports.getModule = class WebServerModule extends ServerModule {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
accepted(resp, body = '', headers = { 'Content-Type:': 'text/html' }) {
|
||||||
|
resp.writeHead(202, 'Accepted', body ? headers : null);
|
||||||
|
return resp.end(body);
|
||||||
|
}
|
||||||
|
|
||||||
badRequest(resp) {
|
badRequest(resp) {
|
||||||
return this.respondWithError(resp, 400, 'Bad request.', 'Bad Request');
|
return this.respondWithError(resp, 400, 'Bad request.', 'Bad Request');
|
||||||
}
|
}
|
||||||
|
|
|
@ -299,11 +299,8 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
|
||||||
async.forEach(
|
async.forEach(
|
||||||
toActors,
|
toActors,
|
||||||
(actorId, nextActor) => {
|
(actorId, nextActor) => {
|
||||||
// :TODO: verify this - if *any* audience/actor is public, then this message is public I believe.
|
|
||||||
if (Collection.PublicCollectionId === actorId) {
|
if (Collection.PublicCollectionId === actorId) {
|
||||||
// Deliver to inbox for "everyone":
|
// :TODO: we should probably land this in a public areaTag as well for AP; allowing Message objects to be used/etc.
|
||||||
// - Add to 'sharedInbox' collection
|
|
||||||
//
|
|
||||||
Collection.addPublicInboxItem(note, err => {
|
Collection.addPublicInboxItem(note, err => {
|
||||||
return nextActor(err);
|
return nextActor(err);
|
||||||
});
|
});
|
||||||
|
@ -312,29 +309,23 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
|
||||||
req,
|
req,
|
||||||
resp,
|
resp,
|
||||||
actorId,
|
actorId,
|
||||||
|
activity,
|
||||||
note,
|
note,
|
||||||
nextActor
|
nextActor
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
err => {
|
err => {
|
||||||
if (err) {
|
if (err && 'SQLITE_CONSTRAINT' !== err.code) {
|
||||||
// if we get a dupe, just tell the remote everything is A-OK
|
|
||||||
if ('SQLITE_CONSTRAINT' === err.code) {
|
|
||||||
resp.writeHead(202);
|
|
||||||
return resp.end('');
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.webServer.internalServerError(resp, err);
|
return this.webServer.internalServerError(resp, err);
|
||||||
}
|
}
|
||||||
|
|
||||||
resp.writeHead(202);
|
return this.webServer.accepted(resp);
|
||||||
return resp.end('');
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_deliverInboxNoteToLocalActor(req, resp, actorId, note, cb) {
|
_deliverInboxNoteToLocalActor(req, resp, actorId, activity, note, cb) {
|
||||||
const localUserName = accountFromSelfUrl(actorId);
|
const localUserName = accountFromSelfUrl(actorId);
|
||||||
if (!localUserName) {
|
if (!localUserName) {
|
||||||
this.log.debug({ url: req.url }, 'Could not get username from URL');
|
this.log.debug({ url: req.url }, 'Could not get username from URL');
|
||||||
|
@ -360,6 +351,7 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
|
||||||
//
|
//
|
||||||
const messageOpts = {
|
const messageOpts = {
|
||||||
// Notes can have 1:N 'to' relationships while a Message is 1:1;
|
// Notes can have 1:N 'to' relationships while a Message is 1:1;
|
||||||
|
activityId: activity.id,
|
||||||
toUser: localUser,
|
toUser: localUser,
|
||||||
areaTag: Message.WellKnownAreaTags.Private,
|
areaTag: Message.WellKnownAreaTags.Private,
|
||||||
};
|
};
|
||||||
|
@ -564,8 +556,7 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
|
||||||
'Undo "Follow" (un-follow) success'
|
'Undo "Follow" (un-follow) success'
|
||||||
);
|
);
|
||||||
|
|
||||||
resp.writeHead(202);
|
return this.webServer.accepted(resp);
|
||||||
return resp.end('');
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -43,6 +43,7 @@
|
||||||
"graceful-fs": "^4.2.10",
|
"graceful-fs": "^4.2.10",
|
||||||
"hashids": "^2.2.10",
|
"hashids": "^2.2.10",
|
||||||
"hjson": "3.2.2",
|
"hjson": "3.2.2",
|
||||||
|
"html-entities": "^2.3.3",
|
||||||
"http-signature": "^1.3.6",
|
"http-signature": "^1.3.6",
|
||||||
"iconv-lite": "0.6.3",
|
"iconv-lite": "0.6.3",
|
||||||
"ini-config-parser": "^1.0.4",
|
"ini-config-parser": "^1.0.4",
|
||||||
|
|
|
@ -1230,6 +1230,11 @@ hjson@3.2.2:
|
||||||
resolved "https://registry.npmjs.org/hjson/-/hjson-3.2.2.tgz"
|
resolved "https://registry.npmjs.org/hjson/-/hjson-3.2.2.tgz"
|
||||||
integrity sha512-MkUeB0cTIlppeSsndgESkfFD21T2nXPRaBStLtf3cAYA2bVEFdXlodZB0TukwZiobPD1Ksax5DK4RTZeaXCI3Q==
|
integrity sha512-MkUeB0cTIlppeSsndgESkfFD21T2nXPRaBStLtf3cAYA2bVEFdXlodZB0TukwZiobPD1Ksax5DK4RTZeaXCI3Q==
|
||||||
|
|
||||||
|
html-entities@^2.3.3:
|
||||||
|
version "2.3.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-2.3.3.tgz#117d7626bece327fc8baace8868fa6f5ef856e46"
|
||||||
|
integrity sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==
|
||||||
|
|
||||||
http-cache-semantics@^4.1.0:
|
http-cache-semantics@^4.1.0:
|
||||||
version "4.1.0"
|
version "4.1.0"
|
||||||
resolved "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz"
|
resolved "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz"
|
||||||
|
|
Loading…
Reference in New Issue