diff --git a/core/activitypub/collection.js b/core/activitypub/collection.js index fb5488a6..493eec14 100644 --- a/core/activitypub/collection.js +++ b/core/activitypub/collection.js @@ -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); diff --git a/core/activitypub/note.js b/core/activitypub/note.js index a2f658fa..6b86e37a 100644 --- a/core/activitypub/note.js +++ b/core/activitypub/note.js @@ -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; + } }; diff --git a/core/activitypub/util.js b/core/activitypub/util.js index 7a0f0d8a..41d47558 100644 --- a/core/activitypub/util.js +++ b/core/activitypub/util.js @@ -211,8 +211,9 @@ function messageBodyToHtml(body) { } function htmlToMessageBody(html) { - //
,
, and
-> \r\n - html = html.replace(/<\/?br?\/?>/g, '\r\n'); + //
,
, and
,
-> \r\n + //

-> \r\n + html = html.replace(/(?:<\/?br ?\/?>)|(?:<\/p>)/g, '\r\n'); return striptags(decode(html)); } diff --git a/core/enig_error.js b/core/enig_error.js index 0423703a..c5e9f065 100644 --- a/core/enig_error.js +++ b/core/enig_error.js @@ -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 = { diff --git a/core/scanner_tossers/activitypub.js b/core/scanner_tossers/activitypub.js index 75a5166d..014222f4 100644 --- a/core/scanner_tossers/activitypub.js +++ b/core/scanner_tossers/activitypub.js @@ -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 }, diff --git a/core/servers/content/web_handlers/activitypub.js b/core/servers/content/web_handlers/activitypub.js index f5d33301..94bcc3f1 100644 --- a/core/servers/content/web_handlers/activitypub.js +++ b/core/servers/content/web_handlers/activitypub.js @@ -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,