Add attachment information to messages, fix duplicate handling to respond properly

This commit is contained in:
Bryan Ashby 2023-02-03 15:14:27 -07:00
parent 5cfacf4ff0
commit f97d1844e3
No known key found for this signature in database
GPG Key ID: C2C1B501E4EFD994
6 changed files with 123 additions and 41 deletions

View File

@ -38,7 +38,7 @@ module.exports = class Collection extends ActivityPubObject {
); );
} }
static addFollower(owningUser, followingActor, webServer, cb) { static addFollower(owningUser, followingActor, webServer, ignoreDupes, cb) {
const collectionId = const collectionId =
makeUserUrl(webServer, owningUser, '/ap/collections/') + '/followers'; makeUserUrl(webServer, owningUser, '/ap/collections/') + '/followers';
return Collection.addToCollection( return Collection.addToCollection(
@ -48,11 +48,12 @@ module.exports = class Collection extends ActivityPubObject {
followingActor.id, followingActor.id,
followingActor, followingActor,
false, false,
ignoreDupes,
cb cb
); );
} }
static addFollowRequest(owningUser, requestingActor, webServer, cb) { static addFollowRequest(owningUser, requestingActor, webServer, ignoreDupes, cb) {
const collectionId = const collectionId =
makeUserUrl(webServer, owningUser, '/ap/collections/') + '/follow-requests'; makeUserUrl(webServer, owningUser, '/ap/collections/') + '/follow-requests';
return Collection.addToCollection( return Collection.addToCollection(
@ -62,6 +63,7 @@ module.exports = class Collection extends ActivityPubObject {
requestingActor.id, requestingActor.id,
requestingActor, requestingActor,
true, true,
ignoreDupes,
cb cb
); );
} }
@ -70,7 +72,7 @@ module.exports = class Collection extends ActivityPubObject {
return Collection.publicOrderedById('outbox', collectionId, page, null, cb); return Collection.publicOrderedById('outbox', collectionId, page, null, cb);
} }
static addOutboxItem(owningUser, outboxItem, isPrivate, webServer, cb) { static addOutboxItem(owningUser, outboxItem, isPrivate, webServer, ignoreDupes, cb) {
const collectionId = const collectionId =
makeUserUrl(webServer, owningUser, '/ap/collections/') + '/outbox'; makeUserUrl(webServer, owningUser, '/ap/collections/') + '/outbox';
return Collection.addToCollection( return Collection.addToCollection(
@ -80,11 +82,12 @@ module.exports = class Collection extends ActivityPubObject {
outboxItem.id, outboxItem.id,
outboxItem, outboxItem,
isPrivate, isPrivate,
ignoreDupes,
cb cb
); );
} }
static addInboxItem(inboxItem, owningUser, webServer, cb) { static addInboxItem(inboxItem, owningUser, webServer, ignoreDupes, cb) {
const collectionId = const collectionId =
makeUserUrl(webServer, owningUser, '/ap/collections/') + '/inbox'; makeUserUrl(webServer, owningUser, '/ap/collections/') + '/inbox';
return Collection.addToCollection( return Collection.addToCollection(
@ -94,11 +97,12 @@ module.exports = class Collection extends ActivityPubObject {
inboxItem.id, inboxItem.id,
inboxItem, inboxItem,
true, true,
ignoreDupes,
cb cb
); );
} }
static addPublicInboxItem(inboxItem, cb) { static addPublicInboxItem(inboxItem, ignoreDupes, cb) {
return Collection.addToCollection( return Collection.addToCollection(
'publicInbox', 'publicInbox',
null, // N/A null, // N/A
@ -106,6 +110,7 @@ module.exports = class Collection extends ActivityPubObject {
inboxItem.id, inboxItem.id,
inboxItem, inboxItem,
false, false,
ignoreDupes,
cb cb
); );
} }
@ -325,6 +330,7 @@ module.exports = class Collection extends ActivityPubObject {
objectId, objectId,
obj, obj,
isPrivate, isPrivate,
ignoreDupes,
cb cb
) { ) {
if (!isString(obj)) { if (!isString(obj)) {
@ -361,8 +367,8 @@ module.exports = class Collection extends ActivityPubObject {
], ],
function res(err) { function res(err) {
// non-arrow for 'this' scope // non-arrow for 'this' scope
if (err) { if (err && 'SQLITE_CONSTRAINT' === err.code) {
if ('SQLITE_CONSTRAINT' === err.code) { if (ignoreDupes) {
err = null; // ignore err = null; // ignore
} }
return cb(err); return cb(err);

View File

@ -172,17 +172,54 @@ module.exports = class Note extends ActivityPubObject {
// :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._getSubject(message);
// If the summary is not present, build one using the message itself; // List all attachments
// finally, default to a static subject so there is *something* if if (Array.isArray(this.attachment) && this.attachment.length > 0) {
// all else fails. let attachmentInfoLines = ['--[Attachments]--'];
if (this.summary) { // https://socialhub.activitypub.rocks/t/representing-images/624
message.subject = this.summary; this.attachment.forEach(att => {
} else { switch (att.mediaType) {
let subject = message.message.replace(`@${message.toUserName} `, ''); case 'image/avif':
subject = truncate(subject, { length: 32, omission: '...' }); case 'image/apng':
subject = subject || APDefaultSummary; case 'image/png':
message.subject = subject; case 'image/x-png':
case 'image/jpeg':
case 'image/gif':
case 'image/svg+xml':
case 'image/webp':
{
let imgInfo;
if (att.height && att.width) {
imgInfo = `Image (${att.width} x ${att.height})`;
} else {
imgInfo = 'Image';
}
attachmentInfoLines.push(imgInfo);
}
break;
// :TODO: video
default:
attachmentInfoLines.push(att.mediaType);
}
if (att.name) {
attachmentInfoLines.push(att.name);
}
attachmentInfoLines.push(att.url);
attachmentInfoLines.push('');
attachmentInfoLines.push('');
});
message.message += '\r\n\r\n' + attachmentInfoLines.join('\r\n');
}
// If the Note is marked sensitive, prefix the subject
if (this.sensitive) {
message.subject = `[NSFW] ${message.subject}`;
} }
try { try {
@ -233,4 +270,28 @@ module.exports = class Note extends ActivityPubObject {
} }
}); });
} }
_getSubject(message) {
if (this.summary) {
return this.summary;
}
//
// Build a subject from the message itself:
// - First few characters of the message, removing the @username
// prefix, if any
// - Truncate at the first line feed, the end of the message,
// or 32 characters in length, whichever comes first
// - If not end of string, we'll sub in '...'
//
let subject = message.message.replace(`@${message.toUserName} `, '').trim();
const m = /^(.+)\r?\n/.exec(subject);
if (m && m[1]) {
subject = m[1];
}
subject = truncate(subject, { length: 32, omission: '...' });
subject = subject || APDefaultSummary;
return subject;
}
}; };

View File

@ -211,8 +211,9 @@ function messageBodyToHtml(body) {
} }
function htmlToMessageBody(html) { function htmlToMessageBody(html) {
// <br>, </br>, and <br/> -> \r\n // <br>, </br>, and <br />, <br/> -> \r\n
html = html.replace(/<\/?br?\/?>/g, '\r\n'); // </p> -> \r\n
html = html.replace(/(?:<\/?br ?\/?>)|(?:<\/p>)/g, '\r\n');
return striptags(decode(html)); return striptags(decode(html));
} }

View File

@ -59,6 +59,8 @@ exports.Errors = {
Expired: (reason, reasonCode) => new EnigError('Expired', -32015, reason, reasonCode), Expired: (reason, reasonCode) => new EnigError('Expired', -32015, reason, reasonCode),
BadFormData: (reason, reasonCode) => BadFormData: (reason, reasonCode) =>
new EnigError('Bad or missing form data', -32016, reason, reasonCode), new EnigError('Bad or missing form data', -32016, reason, reasonCode),
Duplicate: (reason, reasonCode) =>
new EnigError('Duplicate', -32017, reason, reasonCode),
}; };
exports.ErrorReasons = { exports.ErrorReasons = {

View File

@ -100,6 +100,7 @@ exports.getModule = class ActivityPubScannerTosser extends MessageScanTossModule
activity, activity,
message.isPrivate(), message.isPrivate(),
this._webServer(), this._webServer(),
false, // do not ignore dupes
(err, localId) => { (err, localId) => {
if (!err) { if (!err) {
this.log.debug( this.log.debug(
@ -145,11 +146,16 @@ exports.getModule = class ActivityPubScannerTosser extends MessageScanTossModule
}, },
], ],
(err, activity) => { (err, activity) => {
// dupes aren't considered failure
if (err) { if (err) {
this.log.error( if (err.code === 'SQLITE_CONSTRAINT') {
{ error: err.message, messageId: message.messageId }, this.log.debug({ id: activity.id }, 'Ignoring duplicate');
'Failed to export message to ActivityPub' } else {
); this.log.error(
{ error: err.message, messageId: message.messageId },
'Failed to export message to ActivityPub'
);
}
} else { } else {
this.log.info( this.log.info(
{ id: activity.id }, { id: activity.id },

View File

@ -281,14 +281,6 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
_sharedInboxCreateActivity(req, resp, activity) { _sharedInboxCreateActivity(req, resp, activity) {
const deliverTo = activity.recipientIds(); const deliverTo = activity.recipientIds();
//Create a method to gather all to, cc, bcc, etc. dests (see spec) -> single array
// loop through, and attempt to fetch user-by-actor id for each; if found, deliver
// --we may need to add properties for ActivityPubFollowersId, ActivityPubFollowingId, etc.
// to user props for quick lookup -> user
// special handling of bcc (remove others before delivery), etc.
// const toActorIds = activity.recipientActorIds()
const createWhat = _.get(activity, 'object.type'); const createWhat = _.get(activity, 'object.type');
switch (createWhat) { switch (createWhat) {
case 'Note': case 'Note':
@ -322,7 +314,7 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
switch (actorId) { switch (actorId) {
case Collection.PublicCollectionId: case Collection.PublicCollectionId:
// :TODO: we should probably land this in a public areaTag as well for AP; allowing Message objects to be used/etc. // :TODO: we should probably land this in a public areaTag as well for AP; allowing Message objects to be used/etc.
Collection.addPublicInboxItem(note, err => { Collection.addPublicInboxItem(note, true, err => {
return nextActor(err); return nextActor(err);
}); });
break; break;
@ -342,7 +334,7 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
} }
}, },
err => { err => {
if (err) { if (err && err.code !== 'SQLITE_CONSTRAINT') {
return this.webServer.internalServerError(resp, err); return this.webServer.internalServerError(resp, err);
} }
@ -357,7 +349,7 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
return cb(null); // not found/etc., just bail return cb(null); // not found/etc., just bail
} }
Collection.addInboxItem(note, localUser, this.webServer, err => { Collection.addInboxItem(note, localUser, this.webServer, false, err => {
if (err) { if (err) {
return cb(err); return cb(err);
} }
@ -388,6 +380,8 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
}, },
'Note delivered as message to private mailbox' 'Note delivered as message to private mailbox'
); );
} else if (err.code === 'SQLITE_CONSTRAINT') {
return cb(null);
} }
return cb(err); return cb(err);
}); });
@ -517,13 +511,19 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
this._recordAcceptedFollowRequest(localUser, remoteActor, activity); this._recordAcceptedFollowRequest(localUser, remoteActor, activity);
return ok(); return ok();
} else { } else {
Collection.addFollowRequest(localUser, remoteActor, this.webServer, err => { Collection.addFollowRequest(
if (err) { localUser,
return this.internalServerError(resp, err); remoteActor,
} this.webServer,
true, // ignore dupes
err => {
if (err) {
return this.internalServerError(resp, err);
}
return ok(); return ok();
}); }
);
} }
} }
@ -631,6 +631,7 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
localUser, localUser,
remoteActor, remoteActor,
this.webServer, this.webServer,
true, // ignore dupes
callback callback
); );
}, },
@ -706,7 +707,7 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
} }
_selfAsActorHandler(localUser, localActor, req, resp) { _selfAsActorHandler(localUser, localActor, req, resp) {
this.log.trace( this.log.info(
{ username: localUser.username }, { username: localUser.username },
`Serving ActivityPub Actor for "${localUser.username}"` `Serving ActivityPub Actor for "${localUser.username}"`
); );
@ -744,6 +745,11 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
return this.webServer.resourceNotFound(resp); return this.webServer.resourceNotFound(resp);
} }
this.log.info(
{ username: localUser.username },
`Serving ActivityPub Profile for "${localUser.username}"`
);
const headers = { const headers = {
'Content-Type': contentType, 'Content-Type': contentType,
'Content-Length': Buffer(body).length, 'Content-Length': Buffer(body).length,