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 =
makeUserUrl(webServer, owningUser, '/ap/collections/') + '/followers';
return Collection.addToCollection(
@ -48,11 +48,12 @@ module.exports = class Collection extends ActivityPubObject {
followingActor.id,
followingActor,
false,
ignoreDupes,
cb
);
}
static addFollowRequest(owningUser, requestingActor, webServer, cb) {
static addFollowRequest(owningUser, requestingActor, webServer, ignoreDupes, cb) {
const collectionId =
makeUserUrl(webServer, owningUser, '/ap/collections/') + '/follow-requests';
return Collection.addToCollection(
@ -62,6 +63,7 @@ module.exports = class Collection extends ActivityPubObject {
requestingActor.id,
requestingActor,
true,
ignoreDupes,
cb
);
}
@ -70,7 +72,7 @@ module.exports = class Collection extends ActivityPubObject {
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 =
makeUserUrl(webServer, owningUser, '/ap/collections/') + '/outbox';
return Collection.addToCollection(
@ -80,11 +82,12 @@ module.exports = class Collection extends ActivityPubObject {
outboxItem.id,
outboxItem,
isPrivate,
ignoreDupes,
cb
);
}
static addInboxItem(inboxItem, owningUser, webServer, cb) {
static addInboxItem(inboxItem, owningUser, webServer, ignoreDupes, cb) {
const collectionId =
makeUserUrl(webServer, owningUser, '/ap/collections/') + '/inbox';
return Collection.addToCollection(
@ -94,11 +97,12 @@ module.exports = class Collection extends ActivityPubObject {
inboxItem.id,
inboxItem,
true,
ignoreDupes,
cb
);
}
static addPublicInboxItem(inboxItem, cb) {
static addPublicInboxItem(inboxItem, ignoreDupes, cb) {
return Collection.addToCollection(
'publicInbox',
null, // N/A
@ -106,6 +110,7 @@ module.exports = class Collection extends ActivityPubObject {
inboxItem.id,
inboxItem,
false,
ignoreDupes,
cb
);
}
@ -325,6 +330,7 @@ module.exports = class Collection extends ActivityPubObject {
objectId,
obj,
isPrivate,
ignoreDupes,
cb
) {
if (!isString(obj)) {
@ -361,8 +367,8 @@ module.exports = class Collection extends ActivityPubObject {
],
function res(err) {
// non-arrow for 'this' scope
if (err) {
if ('SQLITE_CONSTRAINT' === err.code) {
if (err && 'SQLITE_CONSTRAINT' === err.code) {
if (ignoreDupes) {
err = null; // ignore
}
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
message.message = htmlToMessageBody(this.content);
message.subject = this._getSubject(message);
// If the summary is not present, build one using the message itself;
// finally, default to a static subject so there is *something* if
// all else fails.
if (this.summary) {
message.subject = this.summary;
} else {
let subject = message.message.replace(`@${message.toUserName} `, '');
subject = truncate(subject, { length: 32, omission: '...' });
subject = subject || APDefaultSummary;
message.subject = subject;
// List all attachments
if (Array.isArray(this.attachment) && this.attachment.length > 0) {
let attachmentInfoLines = ['--[Attachments]--'];
// https://socialhub.activitypub.rocks/t/representing-images/624
this.attachment.forEach(att => {
switch (att.mediaType) {
case 'image/avif':
case 'image/apng':
case 'image/png':
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 {
@ -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) {
// <br>, </br>, and <br/> -> \r\n
html = html.replace(/<\/?br?\/?>/g, '\r\n');
// <br>, </br>, and <br />, <br/> -> \r\n
// </p> -> \r\n
html = html.replace(/(?:<\/?br ?\/?>)|(?:<\/p>)/g, '\r\n');
return striptags(decode(html));
}

View File

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

View File

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

View File

@ -281,14 +281,6 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
_sharedInboxCreateActivity(req, resp, activity) {
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');
switch (createWhat) {
case 'Note':
@ -322,7 +314,7 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
switch (actorId) {
case Collection.PublicCollectionId:
// :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);
});
break;
@ -342,7 +334,7 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
}
},
err => {
if (err) {
if (err && err.code !== 'SQLITE_CONSTRAINT') {
return this.webServer.internalServerError(resp, err);
}
@ -357,7 +349,7 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
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) {
return cb(err);
}
@ -388,6 +380,8 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
},
'Note delivered as message to private mailbox'
);
} else if (err.code === 'SQLITE_CONSTRAINT') {
return cb(null);
}
return cb(err);
});
@ -517,13 +511,19 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
this._recordAcceptedFollowRequest(localUser, remoteActor, activity);
return ok();
} else {
Collection.addFollowRequest(localUser, remoteActor, this.webServer, err => {
if (err) {
return this.internalServerError(resp, err);
}
Collection.addFollowRequest(
localUser,
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,
remoteActor,
this.webServer,
true, // ignore dupes
callback
);
},
@ -706,7 +707,7 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
}
_selfAsActorHandler(localUser, localActor, req, resp) {
this.log.trace(
this.log.info(
{ username: localUser.username },
`Serving ActivityPub Actor for "${localUser.username}"`
);
@ -744,6 +745,11 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
return this.webServer.resourceNotFound(resp);
}
this.log.info(
{ username: localUser.username },
`Serving ActivityPub Profile for "${localUser.username}"`
);
const headers = {
'Content-Type': contentType,
'Content-Length': Buffer(body).length,