2023-01-07 20:48:12 +00:00
|
|
|
/* jslint node: true */
|
|
|
|
'use strict';
|
|
|
|
|
|
|
|
// ENiGMA½
|
2023-01-13 01:49:13 +00:00
|
|
|
const { Errors } = require('../enig_error.js');
|
|
|
|
const UserProps = require('../user_property');
|
2023-01-08 20:42:52 +00:00
|
|
|
const {
|
|
|
|
webFingerProfileUrl,
|
|
|
|
makeUserUrl,
|
|
|
|
selfUrl,
|
|
|
|
isValidLink,
|
2023-01-13 01:49:13 +00:00
|
|
|
} = require('../activitypub/util');
|
2023-01-20 05:31:14 +00:00
|
|
|
const { ActivityStreamsContext } = require('./const');
|
2023-01-13 01:49:13 +00:00
|
|
|
const Log = require('../logger').log;
|
|
|
|
const { queryWebFinger } = require('../webfinger');
|
|
|
|
const EnigAssert = require('../enigma_assert');
|
2023-01-14 04:27:02 +00:00
|
|
|
const ActivityPubSettings = require('./settings');
|
2023-01-21 08:19:19 +00:00
|
|
|
const ActivityPubObject = require('./object');
|
2023-01-07 20:48:12 +00:00
|
|
|
|
|
|
|
// deps
|
|
|
|
const _ = require('lodash');
|
2023-01-14 04:27:02 +00:00
|
|
|
const mimeTypes = require('mime-types');
|
2023-01-21 05:15:59 +00:00
|
|
|
const { getJson } = require('../http_util.js');
|
2023-01-08 20:18:50 +00:00
|
|
|
|
|
|
|
// https://www.w3.org/TR/activitypub/#actor-objects
|
2023-01-21 08:19:19 +00:00
|
|
|
module.exports = class Actor extends ActivityPubObject {
|
2023-01-08 20:18:50 +00:00
|
|
|
constructor(obj) {
|
2023-01-21 08:19:19 +00:00
|
|
|
super(obj);
|
2023-01-07 20:48:12 +00:00
|
|
|
}
|
|
|
|
|
2023-01-08 20:18:50 +00:00
|
|
|
isValid() {
|
2023-01-21 08:19:19 +00:00
|
|
|
if (!super.isValid()) {
|
2023-01-08 20:18:50 +00:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2023-01-22 01:51:54 +00:00
|
|
|
if (!Actor.WellKnownActorTypes.includes(this.type)) {
|
2023-01-08 20:18:50 +00:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2023-01-22 01:51:54 +00:00
|
|
|
const linksValid = Actor.WellKnownLinkTypes.every(l => {
|
|
|
|
// must be valid if present & non-empty
|
2023-01-21 08:19:19 +00:00
|
|
|
if (this[l] && !isValidLink(this[l])) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
return true;
|
2023-01-08 20:18:50 +00:00
|
|
|
});
|
2023-01-21 08:19:19 +00:00
|
|
|
|
2023-01-08 20:18:50 +00:00
|
|
|
if (!linksValid) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2023-01-22 01:51:54 +00:00
|
|
|
static get WellKnownActorTypes() {
|
|
|
|
return ['Person', 'Group', 'Organization', 'Service', 'Application'];
|
|
|
|
}
|
|
|
|
|
|
|
|
static get WellKnownLinkTypes() {
|
|
|
|
return ['inbox', 'outbox', 'following', 'followers'];
|
|
|
|
}
|
|
|
|
|
2023-01-08 20:42:52 +00:00
|
|
|
static fromLocalUser(user, webServer, cb) {
|
|
|
|
const userSelfUrl = selfUrl(webServer, user);
|
2023-01-14 04:27:02 +00:00
|
|
|
const userSettings = ActivityPubSettings.fromUser(user);
|
|
|
|
|
|
|
|
const addImage = (o, t) => {
|
|
|
|
const url = userSettings[t];
|
|
|
|
if (url) {
|
|
|
|
const mt = mimeTypes.contentType(url);
|
|
|
|
if (mt) {
|
|
|
|
o[t] = {
|
|
|
|
mediaType: mt,
|
|
|
|
type: 'Image',
|
|
|
|
url,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
2023-01-08 20:42:52 +00:00
|
|
|
|
|
|
|
const obj = {
|
|
|
|
'@context': [
|
2023-01-09 00:11:49 +00:00
|
|
|
ActivityStreamsContext,
|
2023-01-08 20:42:52 +00:00
|
|
|
'https://w3id.org/security/v1', // :TODO: add support
|
|
|
|
],
|
|
|
|
id: userSelfUrl,
|
|
|
|
type: 'Person',
|
|
|
|
preferredUsername: user.username,
|
|
|
|
name: user.getSanitizedName('real'),
|
|
|
|
endpoints: {
|
|
|
|
sharedInbox: 'TODO',
|
|
|
|
},
|
|
|
|
inbox: makeUserUrl(webServer, user, '/ap/users/') + '/inbox',
|
|
|
|
outbox: makeUserUrl(webServer, user, '/ap/users/') + '/outbox',
|
|
|
|
followers: makeUserUrl(webServer, user, '/ap/users/') + '/followers',
|
|
|
|
following: makeUserUrl(webServer, user, '/ap/users/') + '/following',
|
|
|
|
summary: user.getProperty(UserProps.AutoSignature) || '',
|
|
|
|
url: webFingerProfileUrl(webServer, user),
|
2023-01-14 04:27:02 +00:00
|
|
|
manuallyApprovesFollowers: userSettings.manuallyApprovesFollowers,
|
|
|
|
discoverable: userSettings.discoverable,
|
2023-01-08 20:42:52 +00:00
|
|
|
// :TODO: we can start to define BBS related stuff with the community perhaps
|
|
|
|
// attachment: [
|
|
|
|
// {
|
|
|
|
// name: 'SomeNetwork Address',
|
|
|
|
// type: 'PropertyValue',
|
|
|
|
// value: 'Mateo@21:1/121',
|
|
|
|
// },
|
|
|
|
// ],
|
|
|
|
};
|
|
|
|
|
2023-01-22 03:57:22 +00:00
|
|
|
addImage(obj, 'icon');
|
|
|
|
addImage(obj, 'image');
|
2023-01-14 04:27:02 +00:00
|
|
|
|
2023-01-13 19:07:06 +00:00
|
|
|
const publicKeyPem = user.getProperty(UserProps.PublicActivityPubSigningKey);
|
2023-01-08 20:42:52 +00:00
|
|
|
if (!_.isEmpty(publicKeyPem)) {
|
|
|
|
obj.publicKey = {
|
|
|
|
id: userSelfUrl + '#main-key',
|
|
|
|
owner: userSelfUrl,
|
|
|
|
publicKeyPem,
|
|
|
|
};
|
2023-01-12 05:37:09 +00:00
|
|
|
|
|
|
|
EnigAssert(
|
2023-01-13 19:07:06 +00:00
|
|
|
!_.isEmpty(user.getProperty(UserProps.PrivateActivityPubSigningKey)),
|
2023-01-12 05:37:09 +00:00
|
|
|
'User has public key but no private key!'
|
|
|
|
);
|
2023-01-08 20:42:52 +00:00
|
|
|
} else {
|
|
|
|
Log.warn(
|
|
|
|
{ username: user.username },
|
2023-01-13 19:07:06 +00:00
|
|
|
`No public key (${UserProps.PublicActivityPubSigningKey}) for user "${user.username}"`
|
2023-01-08 20:42:52 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
return cb(null, new Actor(obj));
|
|
|
|
}
|
2023-01-08 20:26:52 +00:00
|
|
|
|
|
|
|
static fromRemoteUrl(url, cb) {
|
2023-01-21 08:19:19 +00:00
|
|
|
// :TODO: cache first
|
2023-01-08 20:18:50 +00:00
|
|
|
const headers = {
|
|
|
|
Accept: 'application/activity+json',
|
|
|
|
};
|
|
|
|
|
2023-01-21 05:15:59 +00:00
|
|
|
getJson(url, { headers }, (err, actor) => {
|
|
|
|
if (err) {
|
|
|
|
return cb(err);
|
2023-01-08 20:18:50 +00:00
|
|
|
}
|
|
|
|
|
2023-01-21 05:15:59 +00:00
|
|
|
actor = new Actor(actor);
|
2023-01-08 20:18:50 +00:00
|
|
|
|
2023-01-21 05:15:59 +00:00
|
|
|
if (!actor.isValid()) {
|
|
|
|
return cb(Errors.Invalid('Invalid Actor'));
|
|
|
|
}
|
2023-01-08 20:18:50 +00:00
|
|
|
|
2023-01-21 05:15:59 +00:00
|
|
|
return cb(null, actor);
|
2023-01-08 20:18:50 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2023-01-12 05:37:09 +00:00
|
|
|
static fromAccountName(actorName, options, cb) {
|
|
|
|
// :TODO: cache first -- do we have an Actor for this account already with a OK TTL?
|
|
|
|
|
|
|
|
queryWebFinger(actorName, (err, res) => {
|
|
|
|
if (err) {
|
|
|
|
return cb(err);
|
|
|
|
}
|
|
|
|
|
|
|
|
// we need a link with 'application/activity+json'
|
|
|
|
const links = res.links;
|
|
|
|
if (!Array.isArray(links)) {
|
|
|
|
return cb(Errors.DoesNotExist('No "links" object in WebFinger response'));
|
|
|
|
}
|
|
|
|
|
|
|
|
const activityLink = links.find(l => {
|
|
|
|
return l.type === 'application/activity+json' && l.href?.length > 0;
|
|
|
|
});
|
|
|
|
|
|
|
|
if (!activityLink) {
|
|
|
|
return cb(
|
|
|
|
Errors.DoesNotExist('No Activity link found in WebFinger response')
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
// we can now query the href value for an Actor
|
|
|
|
return Actor.fromRemoteUrl(activityLink.href, cb);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2023-01-13 16:52:21 +00:00
|
|
|
static fromJsonString(json) {
|
2023-01-08 20:18:50 +00:00
|
|
|
const parsed = JSON.parse(json);
|
|
|
|
return new Actor(parsed);
|
|
|
|
}
|
2023-01-07 20:48:12 +00:00
|
|
|
};
|