diff --git a/core/activitypub/note.js b/core/activitypub/note.js index 9c82b3af..61acc5ef 100644 --- a/core/activitypub/note.js +++ b/core/activitypub/note.js @@ -171,10 +171,18 @@ 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.summary || - truncate(message.message, { length: 32, omission: '...' }) || - APDefaultSummary; + + // 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; + } try { message.modTimestamp = moment(this.published); diff --git a/core/mail_util.js b/core/mail_util.js index 8205c449..3ab6bb9f 100644 --- a/core/mail_util.js +++ b/core/mail_util.js @@ -3,12 +3,13 @@ const EnigmaAssert = require('./enigma_assert.js'); const Address = require('./ftn_address.js'); -const { AddressFlavor } = require('./message.js'); -const Message = require('./message.js'); +const MessageConst = require('./message_const'); +const { getQuotePrefix } = require('./ftn_util'); exports.getAddressedToInfo = getAddressedToInfo; exports.setExternalAddressedToInfo = setExternalAddressedToInfo; exports.copyExternalAddressedToInfo = copyExternalAddressedToInfo; +exports.getQuotePrefixFromName = getQuotePrefixFromName; const EMAIL_REGEX = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[?[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}]?)|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; @@ -35,29 +36,29 @@ function getAddressedToInfo(input) { if (firstAtPos < 0) { let addr = Address.fromString(input); if (Address.isValidAddress(addr)) { - return { flavor: AddressFlavor.FTN, remote: input }; + return { flavor: MessageConst.AddressFlavor.FTN, remote: input }; } const lessThanPos = input.indexOf('<'); if (lessThanPos < 0) { - return { name: input, flavor: AddressFlavor.Local }; + return { name: input, flavor: MessageConst.AddressFlavor.Local }; } const greaterThanPos = input.indexOf('>'); if (greaterThanPos < lessThanPos) { - return { name: input, flavor: AddressFlavor.Local }; + return { name: input, flavor: MessageConst.AddressFlavor.Local }; } addr = Address.fromString(input.slice(lessThanPos + 1, greaterThanPos)); if (Address.isValidAddress(addr)) { return { name: input.slice(0, lessThanPos).trim(), - flavor: AddressFlavor.FTN, + flavor: MessageConst.AddressFlavor.FTN, remote: addr.toString(), }; } - return { name: input, flavor: AddressFlavor.Local }; + return { name: input, flavor: MessageConst.AddressFlavor.Local }; } if (firstAtPos === 0) { @@ -67,7 +68,7 @@ function getAddressedToInfo(input) { if (m) { return { name: input.slice(1, secondAtPos), - flavor: AddressFlavor.ActivityPub, + flavor: MessageConst.AddressFlavor.ActivityPub, remote: input.slice(firstAtPos), }; } @@ -82,38 +83,38 @@ function getAddressedToInfo(input) { if (m) { return { name: input.slice(0, lessThanPos).trim(), - flavor: AddressFlavor.Email, + flavor: MessageConst.AddressFlavor.Email, remote: addr, }; } - return { name: input, flavor: AddressFlavor.Local }; + return { name: input, flavor: MessageConst.AddressFlavor.Local }; } let m = input.match(EMAIL_REGEX); if (m) { return { name: input.slice(0, firstAtPos), - flavor: AddressFlavor.Email, + flavor: MessageConst.AddressFlavor.Email, remote: input, }; } let addr = Address.fromString(input); // 5D? if (Address.isValidAddress(addr)) { - return { flavor: AddressFlavor.FTN, remote: addr.toString() }; + return { flavor: MessageConst.AddressFlavor.FTN, remote: addr.toString() }; } addr = Address.fromString(input.slice(firstAtPos + 1).trim()); if (Address.isValidAddress(addr)) { return { name: input.slice(0, firstAtPos).trim(), - flavor: AddressFlavor.FTN, + flavor: MessageConst.AddressFlavor.FTN, remote: addr.toString(), }; } - return { name: input, flavor: AddressFlavor.Local }; + return { name: input, flavor: MessageConst.AddressFlavor.Local }; } /// returns true if it's an external address @@ -123,11 +124,11 @@ function setExternalAddressedToInfo(addressInfo, message) { }; switch (addressInfo.flavor) { - case AddressFlavor.FTN: - case AddressFlavor.Email: - case AddressFlavor.QWK: - case AddressFlavor.NNTP: - case AddressFlavor.ActivityPub: + case MessageConst.AddressFlavor.FTN: + case MessageConst.AddressFlavor.Email: + case MessageConst.AddressFlavor.QWK: + case MessageConst.AddressFlavor.NNTP: + case MessageConst.AddressFlavor.ActivityPub: EnigmaAssert(isValidAddressInfo()); message.setRemoteToUser(addressInfo.remote); @@ -136,13 +137,18 @@ function setExternalAddressedToInfo(addressInfo, message) { return true; default: - case AddressFlavor.Local: + case MessageConst.AddressFlavor.Local: return false; } } function copyExternalAddressedToInfo(fromMessage, toMessage) { - const sm = Message.SystemMetaNames; + const sm = MessageConst.SystemMetaNames; toMessage.setRemoteToUser(fromMessage.meta.System[sm.RemoteFromUser]); toMessage.setExternalFlavor(fromMessage.meta.System[sm.ExternalFlavor]); } + +function getQuotePrefixFromName(name) { + const addrInfo = getAddressedToInfo(name); + return getQuotePrefix(addrInfo.name || name); +} diff --git a/core/message.js b/core/message.js index bb2b6766..31b5621b 100644 --- a/core/message.js +++ b/core/message.js @@ -3,14 +3,14 @@ const msgDb = require('./database.js').dbs.message; const wordWrapText = require('./word_wrap.js').wordWrapText; -const ftnUtil = require('./ftn_util.js'); const createNamedUUID = require('./uuid_util.js').createNamedUUID; const Errors = require('./enig_error.js').Errors; const ANSI = require('./ansi_term.js'); const { sanitizeString, getISOTimestampString } = require('./database.js'); - const { isCP437Encodable } = require('./cp437util'); const { containsNonLatinCodepoints } = require('./string_util'); +const MessageConst = require('./message_const'); +const { getQuotePrefixFromName } = require('./mail_util'); const { isAnsi, @@ -33,93 +33,6 @@ const ENIGMA_MESSAGE_UUID_NAMESPACE = uuidParse.parse( '154506df-1df8-46b9-98f8-ebb5815baaf8' ); -const WELL_KNOWN_AREA_TAGS = { - Invalid: '', - Private: 'private_mail', - Bulletin: 'local_bulletin', -}; - -const WellKnownMetaCategories = { - System: 'System', - FtnProperty: 'FtnProperty', - FtnKludge: 'FtnKludge', - QwkProperty: 'QwkProperty', - QwkKludge: 'QwkKludge', - ActivityPub: 'ActivityPub', -}; - -// Category: WellKnownMetaCategories.System ("System") -const SYSTEM_META_NAMES = { - LocalToUserID: 'local_to_user_id', - LocalFromUserID: 'local_from_user_id', - StateFlags0: 'state_flags0', // See Message.StateFlags0 - ExplicitEncoding: 'explicit_encoding', // Explicitly set encoding when exporting/etc. - ExternalFlavor: 'external_flavor', // "Flavor" of message - imported from or to be exported to. See Message.AddressFlavor - RemoteToUser: 'remote_to_user', // Opaque value depends on external system, e.g. FTN address - RemoteFromUser: 'remote_from_user', // Opaque value depends on external system, e.g. FTN address -}; - -// Types for Message.SystemMetaNames.ExternalFlavor meta -const ADDRESS_FLAVOR = { - Local: 'local', // local / non-remote addressing - FTN: 'ftn', // FTN style - Email: 'email', // From email - QWK: 'qwk', // QWK packet - NNTP: 'nntp', // NNTP article POST; often a email address - ActivityPub: 'activitypub', // ActivityPub, Mastodon, etc. -}; - -const STATE_FLAGS0 = { - None: 0x00000000, - Imported: 0x00000001, // imported from foreign system - Exported: 0x00000002, // exported to foreign system -}; - -// :TODO: these should really live elsewhere... -// Category: WellKnownMetaCategories.FtnProperty ("FtnProperty") -const FTN_PROPERTY_NAMES = { - // packet header oriented - FtnOrigNode: 'ftn_orig_node', - FtnDestNode: 'ftn_dest_node', - // :TODO: rename these to ftn_*_net vs network - ensure things won't break, may need mapping - FtnOrigNetwork: 'ftn_orig_network', - FtnDestNetwork: 'ftn_dest_network', - FtnAttrFlags: 'ftn_attr_flags', - FtnCost: 'ftn_cost', - FtnOrigZone: 'ftn_orig_zone', - FtnDestZone: 'ftn_dest_zone', - FtnOrigPoint: 'ftn_orig_point', - FtnDestPoint: 'ftn_dest_point', - - // message header oriented - FtnMsgOrigNode: 'ftn_msg_orig_node', - FtnMsgDestNode: 'ftn_msg_dest_node', - FtnMsgOrigNet: 'ftn_msg_orig_net', - FtnMsgDestNet: 'ftn_msg_dest_net', - - FtnAttribute: 'ftn_attribute', - - FtnTearLine: 'ftn_tear_line', // http://ftsc.org/docs/fts-0004.001 - FtnOrigin: 'ftn_origin', // http://ftsc.org/docs/fts-0004.001 - FtnArea: 'ftn_area', // http://ftsc.org/docs/fts-0004.001 - FtnSeenBy: 'ftn_seen_by', // http://ftsc.org/docs/fts-0004.001 -}; - -// Category: WellKnownMetaCategories.QwkProperty -const QWKPropertyNames = { - MessageNumber: 'qwk_msg_num', - MessageStatus: 'qwk_msg_status', // See http://wiki.synchro.net/ref:qwk for a decent list - ConferenceNumber: 'qwk_conf_num', - InReplyToNum: 'qwk_in_reply_to_num', // note that we prefer the 'InReplyToMsgId' kludge if available -}; - -// Category: WellKnownMetaCategories.ActivityPub -const ActivityPubPropertyNames = { - ActivityId: 'activitypub_activity_id', // Activity ID; FK to AP table entries - 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)! const MESSAGE_ROW_MAP = { reply_to_message_id: 'replyToMsgId', @@ -247,35 +160,35 @@ module.exports = class Message { } static get WellKnownMetaCategories() { - return WellKnownMetaCategories; + return MessageConst.WellKnownMetaCategories; } static get WellKnownAreaTags() { - return WELL_KNOWN_AREA_TAGS; + return MessageConst.WellKnownAreaTags; } static get SystemMetaNames() { - return SYSTEM_META_NAMES; + return MessageConst.SystemMetaNames; } static get AddressFlavor() { - return ADDRESS_FLAVOR; + return MessageConst.AddressFlavor; } static get StateFlags0() { - return STATE_FLAGS0; + return MessageConst.StateFlags0; } static get FtnPropertyNames() { - return FTN_PROPERTY_NAMES; + return MessageConst.FtnPropertyNames; } static get QWKPropertyNames() { - return QWKPropertyNames; + return MessageConst.QWKPropertyNames; } static get ActivityPubPropertyNames() { - return ActivityPubPropertyNames; + return MessageConst.ActivityPubPropertyNames; } setLocalToUserId(userId) { @@ -943,11 +856,13 @@ module.exports = class Message { ); } - // :TODO: FTN stuff doesn't have any business here - getFTNQuotePrefix(source) { + _getQuotePrefix(source) { source = source || 'fromUserName'; - return ftnUtil.getQuotePrefix(this[source]); + // grab out the name member, so we don't try to build + // quote prefixes such as "@N" for "@NuSkooler@some.host", etc. + const userName = this[source]; + return getQuotePrefixFromName(userName); } static getTearLinePosition(input) { @@ -982,7 +897,7 @@ module.exports = class Message { */ const quotePrefix = options.includePrefix - ? this.getFTNQuotePrefix(options.prefixSource || 'fromUserName') + ? this._getQuotePrefix(options.prefixSource || 'fromUserName') : ''; function getWrapped(text, extraPrefix) { diff --git a/core/message_const.js b/core/message_const.js new file mode 100644 index 00000000..78d83d19 --- /dev/null +++ b/core/message_const.js @@ -0,0 +1,93 @@ +const WellKnownAreaTags = { + Invalid: '', + Private: 'private_mail', + Bulletin: 'local_bulletin', +}; +exports.WellKnownAreaTags = WellKnownAreaTags; + +const WellKnownMetaCategories = { + System: 'System', + FtnProperty: 'FtnProperty', + FtnKludge: 'FtnKludge', + QwkProperty: 'QwkProperty', + QwkKludge: 'QwkKludge', + ActivityPub: 'ActivityPub', +}; +exports.WellKnownMetaCategories = WellKnownMetaCategories; + +// Category: WellKnownMetaCategories.System ("System") +const SystemMetaNames = { + LocalToUserID: 'local_to_user_id', + LocalFromUserID: 'local_from_user_id', + StateFlags0: 'state_flags0', // See Message.StateFlags0 + ExplicitEncoding: 'explicit_encoding', // Explicitly set encoding when exporting/etc. + ExternalFlavor: 'external_flavor', // "Flavor" of message - imported from or to be exported to. See Message.AddressFlavor + RemoteToUser: 'remote_to_user', // Opaque value depends on external system, e.g. FTN address + RemoteFromUser: 'remote_from_user', // Opaque value depends on external system, e.g. FTN address +}; +exports.SystemMetaNames = SystemMetaNames; + +// Types for Message.SystemMetaNames.ExternalFlavor meta +const AddressFlavor = { + Local: 'local', // local / non-remote addressing + FTN: 'ftn', // FTN style + Email: 'email', // From email + QWK: 'qwk', // QWK packet + NNTP: 'nntp', // NNTP article POST; often a email address + ActivityPub: 'activitypub', // ActivityPub, Mastodon, etc. +}; +exports.AddressFlavor = AddressFlavor; + +const StateFlags0 = { + None: 0x00000000, + Imported: 0x00000001, // imported from foreign system + Exported: 0x00000002, // exported to foreign system +}; +exports.StateFlags0 = StateFlags0; + +// Category: WellKnownMetaCategories.FtnProperty ("FtnProperty") +const FtnPropertyNames = { + // packet header oriented + FtnOrigNode: 'ftn_orig_node', + FtnDestNode: 'ftn_dest_node', + // :TODO: rename these to ftn_*_net vs network - ensure things won't break, may need mapping + FtnOrigNetwork: 'ftn_orig_network', + FtnDestNetwork: 'ftn_dest_network', + FtnAttrFlags: 'ftn_attr_flags', + FtnCost: 'ftn_cost', + FtnOrigZone: 'ftn_orig_zone', + FtnDestZone: 'ftn_dest_zone', + FtnOrigPoint: 'ftn_orig_point', + FtnDestPoint: 'ftn_dest_point', + + // message header oriented + FtnMsgOrigNode: 'ftn_msg_orig_node', + FtnMsgDestNode: 'ftn_msg_dest_node', + FtnMsgOrigNet: 'ftn_msg_orig_net', + FtnMsgDestNet: 'ftn_msg_dest_net', + + FtnAttribute: 'ftn_attribute', + + FtnTearLine: 'ftn_tear_line', // http://ftsc.org/docs/fts-0004.001 + FtnOrigin: 'ftn_origin', // http://ftsc.org/docs/fts-0004.001 + FtnArea: 'ftn_area', // http://ftsc.org/docs/fts-0004.001 + FtnSeenBy: 'ftn_seen_by', // http://ftsc.org/docs/fts-0004.001 +}; +exports.FtnPropertyNames = FtnPropertyNames; + +// Category: WellKnownMetaCategories.QwkProperty +const QWKPropertyNames = { + MessageNumber: 'qwk_msg_num', + MessageStatus: 'qwk_msg_status', // See http://wiki.synchro.net/ref:qwk for a decent list + ConferenceNumber: 'qwk_conf_num', + InReplyToNum: 'qwk_in_reply_to_num', // note that we prefer the 'InReplyToMsgId' kludge if available +}; +exports.QWKPropertyNames = QWKPropertyNames; + +// Category: WellKnownMetaCategories.ActivityPub +const ActivityPubPropertyNames = { + ActivityId: 'activitypub_activity_id', // Activity ID; FK to AP table entries + InReplyTo: 'activitypub_in_reply_to', // Activity ID from 'inReplyTo' field + NoteId: 'activitypub_note_id', // Note ID specific to Note Activities +}; +exports.ActivityPubPropertyNames = ActivityPubPropertyNames; diff --git a/core/servers/content/web_handlers/activitypub.js b/core/servers/content/web_handlers/activitypub.js index 7c606862..8dfc3f92 100644 --- a/core/servers/content/web_handlers/activitypub.js +++ b/core/servers/content/web_handlers/activitypub.js @@ -258,7 +258,7 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule { { type: activity.type }, 'Invalid or unknown Activity type' ); - return this.resourceNotFound(resp); + return this.webServer.resourceNotFound(resp); } }); } @@ -279,7 +279,7 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule { { type: createWhat }, 'Invalid or unsupported "Create" type' ); - return this.resourceNotFound(resp); + return this.webServer.resourceNotFound(resp); } }