2023-01-23 21:45:56 +00:00
|
|
|
const Message = require('../message');
|
|
|
|
const ActivityPubObject = require('./object');
|
|
|
|
const { Errors } = require('../enig_error');
|
|
|
|
const { getISOTimestampString } = require('../database');
|
|
|
|
const User = require('../user');
|
2023-01-25 04:40:12 +00:00
|
|
|
const { messageBodyToHtml, htmlToMessageBody } = require('./util');
|
2023-01-26 05:22:45 +00:00
|
|
|
const { isAnsi } = require('../string_util');
|
2023-01-23 21:45:56 +00:00
|
|
|
|
|
|
|
// deps
|
|
|
|
const { v5: UUIDv5 } = require('uuid');
|
|
|
|
const Actor = require('./actor');
|
|
|
|
const moment = require('moment');
|
|
|
|
const Collection = require('./collection');
|
|
|
|
const async = require('async');
|
2023-01-26 05:22:45 +00:00
|
|
|
const { isString, isObject, truncate } = require('lodash');
|
2023-01-23 21:45:56 +00:00
|
|
|
|
|
|
|
const APMessageIdNamespace = '307bc7b3-3735-4573-9a20-e3f9eaac29c5';
|
2023-01-26 01:41:47 +00:00
|
|
|
const APDefaultSummary = '[ActivityPub]';
|
2023-01-23 21:45:56 +00:00
|
|
|
|
|
|
|
module.exports = class Note extends ActivityPubObject {
|
|
|
|
constructor(obj) {
|
|
|
|
super(obj);
|
|
|
|
}
|
|
|
|
|
|
|
|
isValid() {
|
|
|
|
if (!super.isValid()) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// :TODO: validate required properties
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
static fromPublicNoteId(noteId, cb) {
|
|
|
|
Collection.embeddedObjById('outbox', false, noteId, (err, obj) => {
|
|
|
|
if (err) {
|
|
|
|
return cb(err);
|
|
|
|
}
|
|
|
|
|
|
|
|
return cb(null, new Note(obj.object));
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
// A local Message bound for ActivityPub
|
|
|
|
static fromLocalOutgoingMessage(message, webServer, cb) {
|
|
|
|
const localUserId = message.getLocalFromUserId();
|
|
|
|
if (!localUserId) {
|
|
|
|
return cb(Errors.UnexpectedState('Invalid user ID for local user!'));
|
|
|
|
}
|
|
|
|
|
|
|
|
if (Message.AddressFlavor.ActivityPub !== message.getAddressFlavor()) {
|
|
|
|
return cb(
|
|
|
|
Errors.Invalid('Cannot build note for non-ActivityPub addressed message')
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
const remoteActorAccount = message.getRemoteToUser();
|
|
|
|
if (!remoteActorAccount) {
|
|
|
|
return cb(
|
|
|
|
Errors.UnexpectedState('Message does not contain a remote address')
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
async.waterfall(
|
|
|
|
[
|
|
|
|
callback => {
|
|
|
|
return User.getUser(localUserId, callback);
|
|
|
|
},
|
|
|
|
(fromUser, callback) => {
|
|
|
|
Actor.fromLocalUser(fromUser, webServer, (err, fromActor) => {
|
|
|
|
return callback(err, fromUser, fromActor);
|
|
|
|
});
|
|
|
|
},
|
|
|
|
(fromUser, fromActor, callback) => {
|
2023-01-26 01:41:47 +00:00
|
|
|
Actor.fromId(remoteActorAccount, (err, remoteActor) => {
|
2023-01-23 21:45:56 +00:00
|
|
|
return callback(err, fromUser, fromActor, remoteActor);
|
|
|
|
});
|
|
|
|
},
|
|
|
|
(fromUser, fromActor, remoteActor, callback) => {
|
2023-01-26 05:22:45 +00:00
|
|
|
if (!message.replyToMsgId) {
|
|
|
|
return callback(null, null, fromUser, fromActor, remoteActor);
|
|
|
|
}
|
2023-01-23 21:45:56 +00:00
|
|
|
|
2023-01-26 05:22:45 +00:00
|
|
|
Message.getMetaValuesByMessageId(
|
|
|
|
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
|
2023-01-23 21:45:56 +00:00
|
|
|
const obj = {
|
|
|
|
id: ActivityPubObject.makeObjectId(webServer, 'note'),
|
|
|
|
type: 'Note',
|
|
|
|
published: getISOTimestampString(message.modTimestamp),
|
|
|
|
to,
|
|
|
|
attributedTo: fromActor.id,
|
|
|
|
content: messageBodyToHtml(message.message.trim()),
|
2023-01-26 05:22:45 +00:00
|
|
|
source: {
|
|
|
|
content: message.message,
|
|
|
|
mediaType: sourceMediaType,
|
|
|
|
},
|
2023-01-23 21:45:56 +00:00
|
|
|
};
|
|
|
|
|
2023-01-26 05:22:45 +00:00
|
|
|
if (replyToNoteId) {
|
|
|
|
obj.inReplyTo = replyToNoteId;
|
|
|
|
}
|
|
|
|
|
|
|
|
// ignore the subject if it's our default summary value for replies
|
|
|
|
if (message.subject !== `RE: ${APDefaultSummary}`) {
|
2023-01-26 01:41:47 +00:00
|
|
|
obj.summary = message.subject;
|
|
|
|
}
|
|
|
|
|
2023-01-23 21:45:56 +00:00
|
|
|
const note = new Note(obj);
|
|
|
|
return callback(null, { note, fromUser, remoteActor });
|
|
|
|
},
|
|
|
|
],
|
|
|
|
(err, noteInfo) => {
|
|
|
|
return cb(err, noteInfo);
|
|
|
|
}
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2023-01-25 04:40:12 +00:00
|
|
|
toMessage(options, cb) {
|
|
|
|
if (!isObject(options.toUser) || !isString(options.areaTag)) {
|
|
|
|
return cb(Errors.MissingParam('Missing one or more required options!'));
|
|
|
|
}
|
|
|
|
|
2023-01-23 21:45:56 +00:00
|
|
|
// stable ID based on Note ID
|
|
|
|
const message = new Message({
|
|
|
|
uuid: UUIDv5(this.id, APMessageIdNamespace),
|
|
|
|
});
|
|
|
|
|
2023-01-25 04:40:12 +00:00
|
|
|
// Fetch the remote actor info to get their user info
|
2023-01-26 01:41:47 +00:00
|
|
|
Actor.fromId(this.attributedTo, (err, attributedToActor, fromActorSubject) => {
|
2023-01-23 21:45:56 +00:00
|
|
|
if (err) {
|
2023-01-26 01:41:47 +00:00
|
|
|
return cb(err);
|
2023-01-23 21:45:56 +00:00
|
|
|
}
|
|
|
|
|
2023-01-26 01:41:47 +00:00
|
|
|
message.fromUserName = fromActorSubject || this.attributedTo;
|
|
|
|
|
2023-01-25 04:40:12 +00:00
|
|
|
//
|
|
|
|
// Note's can be addressed to 1:N users, but a Message is a 1:1
|
|
|
|
// relationship. This method requires the mapping up front via options
|
|
|
|
//
|
2023-01-26 01:41:47 +00:00
|
|
|
message.toUserName = options.toUser.username;
|
|
|
|
message.meta.System[Message.SystemMetaNames.LocalToUserID] =
|
|
|
|
options.toUser.userId;
|
2023-01-25 04:40:12 +00:00
|
|
|
message.areaTag = options.areaTag || Message.WellKnownAreaTags.Private;
|
|
|
|
|
|
|
|
// :TODO: it would be better to do some basic HTML to ANSI or pipe codes perhaps
|
|
|
|
message.message = htmlToMessageBody(this.content);
|
2023-01-26 05:22:45 +00:00
|
|
|
message.subject =
|
|
|
|
this.summary ||
|
|
|
|
truncate(message.message, { length: 32, omission: '...' }) ||
|
|
|
|
APDefaultSummary;
|
2023-01-23 21:45:56 +00:00
|
|
|
|
|
|
|
try {
|
|
|
|
message.modTimestamp = moment(this.published);
|
|
|
|
} catch (e) {
|
|
|
|
// :TODO: Log warning
|
|
|
|
message.modTimestamp = moment();
|
|
|
|
}
|
|
|
|
|
|
|
|
// :TODO: replyToMsgId from 'inReplyTo'
|
2023-01-25 04:40:12 +00:00
|
|
|
message.setRemoteFromUser(this.attributedTo);
|
|
|
|
message.setExternalFlavor(Message.AddressFlavor.ActivityPub);
|
2023-01-23 21:45:56 +00:00
|
|
|
|
2023-01-25 04:40:12 +00:00
|
|
|
message.meta.ActivityPub = message.meta.ActivityPub || {};
|
|
|
|
message.meta.ActivityPub[Message.ActivityPubPropertyNames.ActivityId] =
|
2023-01-26 05:22:45 +00:00
|
|
|
options.activityId || 0;
|
|
|
|
message.meta.ActivityPub[Message.ActivityPubPropertyNames.NoteId] = this.id;
|
|
|
|
|
|
|
|
if (this.inReplyTo) {
|
2023-01-25 04:40:12 +00:00
|
|
|
message.meta.ActivityPub[Message.ActivityPubPropertyNames.InReplyTo] =
|
2023-01-26 05:22:45 +00:00
|
|
|
this.inReplyTo;
|
2023-01-23 21:45:56 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return cb(null, message);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
};
|