enigma-bbs/core/activitypub_activity.js

201 lines
7.2 KiB
JavaScript
Raw Normal View History

const { isString, isObject } = require('lodash');
const { v4: UUIDv4 } = require('uuid');
const {
ActivityStreamsContext,
messageBodyToHtml,
selfUrl,
} = require('./activitypub_util');
const { Errors } = require('./enig_error');
const User = require('./user');
const Actor = require('./activitypub_actor');
const { getISOTimestampString } = require('./database');
const UserProps = require('./user_property');
const { postJson } = require('./http_util');
// deps
const async = require('async');
const _ = require('lodash');
module.exports = class Activity {
constructor(obj) {
this['@context'] = ActivityStreamsContext;
Object.assign(this, obj);
}
static get ActivityTypes() {
return [
'Create',
'Update',
'Delete',
'Follow',
'Accept',
'Reject',
'Add',
'Remove',
'Like',
'Announce',
'Undo',
];
}
static fromJson(json) {
const parsed = JSON.parse(json);
return new Activity(parsed);
}
isValid() {
if (
this['@context'] !== ActivityStreamsContext ||
!isString(this.id) ||
!isString(this.actor) ||
(!isString(this.object) && !isObject(this.object)) ||
!Activity.ActivityTypes.includes(this.type)
) {
return false;
}
// :TODO: we could validate the particular types
return true;
}
// https://www.w3.org/TR/activitypub/#accept-activity-inbox
static makeAccept(webServer, localActor, followRequest, id = null) {
id = id || Activity._makeId(webServer, '/accept');
return new Activity({
type: 'Accept',
actor: localActor,
object: followRequest, // previous request Activity
});
}
static noteFromLocalMessage(webServer, message, cb) {
const localUserId = message.getLocalFromUserId();
if (!localUserId) {
return cb(Errors.UnexpectedState('Invalid user ID for local user!'));
}
async.waterfall(
[
callback => {
return User.getUser(localUserId, callback);
},
(localUser, callback) => {
const remoteActorAccount = message.getRemoteToUser();
if (!remoteActorAccount) {
return callback(
Errors.UnexpectedState(
'Message does not contain a remote address'
)
);
}
const opts = {};
Actor.fromAccountName(
remoteActorAccount,
opts,
(err, remoteActor) => {
return callback(err, localUser, remoteActor);
}
);
},
(localUser, remoteActor, callback) => {
Actor.fromLocalUser(localUser, webServer, (err, localActor) => {
return callback(err, localUser, localActor, remoteActor);
});
},
(localUser, localActor, remoteActor, callback) => {
// we'll need the entire |activityId| as a linked reference later
const activityId = Activity._makeId(webServer, '/create');
// |remoteActor| is non-null if we fetchd it
//const to = message.isPrivate() ? remoteActor ? remoteActor.id : `${ActivityStreamsContext}#Public`;
// const obj = {
// '@context': ActivityStreamsContext,
// id: activityId,
// type: 'Create',
// to: [remoteActor.id],
// audience: ['as:Public'],
// actor: localActor.id,
// object: {
// id: Activity._makeId(webServer, '/note'),
// type: 'Note',
// attributedTo: localActor.id,
// to: [remoteActor.id],
// audience: ['as:Public'],
// content: messageBodyToHtml(message.message.trim()),
// },
// };
const obj = {
'@context': ActivityStreamsContext,
id: activityId,
type: 'Create',
actor: localActor.id,
object: {
id: Activity._makeId(webServer, '/note'),
type: 'Note',
published: getISOTimestampString(message.modTimestamp),
attributedTo: localActor.id,
// :TODO: inReplyto if this is a reply; we need this store in message meta.
// :TODO: we may want to turn this to a HTML fragment?
content: messageBodyToHtml(message.message.trim()),
},
};
// :TODO: this probably needs to change quite a bit based on "groups"
// :TODO: verify we need both 'to' fields: https://socialhub.activitypub.rocks/t/problems-posting-to-mastodon-inbox/801/4
if (message.isPrivate()) {
obj.to = remoteActor.id;
obj.object.to = remoteActor.id;
} else {
const publicInbox = `${ActivityStreamsContext}#Public`;
obj.to = publicInbox;
obj.object.to = publicInbox;
}
const activity = new Activity(obj);
return callback(null, activity, localUser, remoteActor);
},
],
(err, activity, fromUser, remoteActor) => {
return cb(err, { activity, fromUser, remoteActor });
}
);
}
sendTo(actorUrl, fromUser, webServer, cb) {
const privateKey = fromUser.getProperty(UserProps.PrivateKeyMain);
if (_.isEmpty(privateKey)) {
return cb(
Errors.MissingProperty(
`User "${fromUser.username}" is missing the '${UserProps.PrivateKeyMain}' property`
)
);
}
const reqOpts = {
headers: {
'Content-Type': 'application/activity+json',
},
sign: {
// :TODO: Make a helper for this
key: privateKey,
keyId: selfUrl(webServer, fromUser) + '#main-key',
authorizationHeaderName: 'Signature',
headers: ['(request-target)', 'host', 'date', 'digest', 'content-type'],
},
};
const activityJson = JSON.stringify(this);
return postJson(actorUrl, activityJson, reqOpts, cb);
}
static _makeId(webServer, prefix = '') {
return webServer.buildUrl(`${prefix}/${UUIDv4()}`);
}
};