Re-work of ActivityPub DBs and various account lookups
* Always look up Actors by explicit Actor IDs * Re-work DB: style, properties we track, etc. * Create AP properties via a event! * Lots of cleanup * WF may be partially broken if loooking up by 'profile' alias URL: WIP
This commit is contained in:
parent
8dd28e3091
commit
9b01124b2e
|
@ -1,4 +1,4 @@
|
||||||
const { selfUrl } = require('./util');
|
const { localActorId } = require('./util');
|
||||||
const { WellKnownActivityTypes } = require('./const');
|
const { WellKnownActivityTypes } = require('./const');
|
||||||
const ActivityPubObject = require('./object');
|
const ActivityPubObject = require('./object');
|
||||||
const { Errors } = require('../enig_error');
|
const { Errors } = require('../enig_error');
|
||||||
|
@ -18,6 +18,11 @@ module.exports = class Activity extends ActivityPubObject {
|
||||||
return WellKnownActivityTypes;
|
return WellKnownActivityTypes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static fromJsonString(s) {
|
||||||
|
const obj = ActivityPubObject.fromJsonString(s);
|
||||||
|
return new Activity(obj);
|
||||||
|
}
|
||||||
|
|
||||||
static makeFollow(webServer, localActor, remoteActor) {
|
static makeFollow(webServer, localActor, remoteActor) {
|
||||||
return new Activity({
|
return new Activity({
|
||||||
id: Activity.activityObjectId(webServer),
|
id: Activity.activityObjectId(webServer),
|
||||||
|
@ -74,7 +79,7 @@ module.exports = class Activity extends ActivityPubObject {
|
||||||
sign: {
|
sign: {
|
||||||
// :TODO: Make a helper for this
|
// :TODO: Make a helper for this
|
||||||
key: privateKey,
|
key: privateKey,
|
||||||
keyId: selfUrl(webServer, fromUser) + '#main-key',
|
keyId: localActorId(webServer, fromUser) + '#main-key',
|
||||||
authorizationHeaderName: 'Signature',
|
authorizationHeaderName: 'Signature',
|
||||||
headers: ['(request-target)', 'host', 'date', 'digest', 'content-type'],
|
headers: ['(request-target)', 'host', 'date', 'digest', 'content-type'],
|
||||||
},
|
},
|
||||||
|
@ -84,6 +89,23 @@ module.exports = class Activity extends ActivityPubObject {
|
||||||
return postJson(actorUrl, activityJson, reqOpts, cb);
|
return postJson(actorUrl, activityJson, reqOpts, cb);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
recipientIds() {
|
||||||
|
const ids = [];
|
||||||
|
|
||||||
|
// :TODO: bto, bcc?
|
||||||
|
['to', 'cc', 'audience'].forEach(field => {
|
||||||
|
let v = this[field];
|
||||||
|
if (v) {
|
||||||
|
if (!Array.isArray(v)) {
|
||||||
|
v = [v];
|
||||||
|
}
|
||||||
|
ids.push(...v);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
static activityObjectId(webServer) {
|
static activityObjectId(webServer) {
|
||||||
return ActivityPubObject.makeObjectId(webServer, 'activity');
|
return ActivityPubObject.makeObjectId(webServer, 'activity');
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,6 @@ const {
|
||||||
ActivityStreamsContext,
|
ActivityStreamsContext,
|
||||||
webFingerProfileUrl,
|
webFingerProfileUrl,
|
||||||
makeUserUrl,
|
makeUserUrl,
|
||||||
selfUrl,
|
|
||||||
isValidLink,
|
isValidLink,
|
||||||
makeSharedInboxUrl,
|
makeSharedInboxUrl,
|
||||||
userNameFromSubject,
|
userNameFromSubject,
|
||||||
|
@ -68,7 +67,15 @@ module.exports = class Actor extends ActivityPubObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
static fromLocalUser(user, webServer, cb) {
|
static fromLocalUser(user, webServer, cb) {
|
||||||
const userSelfUrl = selfUrl(webServer, user);
|
const userActorId = user.getProperty(UserProps.ActivityPubActorId);
|
||||||
|
if (!userActorId) {
|
||||||
|
return cb(
|
||||||
|
Errors.MissingProperty(
|
||||||
|
`User missing '${UserProps.ActivityPubActorId}' property`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const userSettings = ActivityPubSettings.fromUser(user);
|
const userSettings = ActivityPubSettings.fromUser(user);
|
||||||
|
|
||||||
const addImage = (o, t) => {
|
const addImage = (o, t) => {
|
||||||
|
@ -90,7 +97,7 @@ module.exports = class Actor extends ActivityPubObject {
|
||||||
ActivityStreamsContext,
|
ActivityStreamsContext,
|
||||||
'https://w3id.org/security/v1', // :TODO: add support
|
'https://w3id.org/security/v1', // :TODO: add support
|
||||||
],
|
],
|
||||||
id: userSelfUrl,
|
id: userActorId,
|
||||||
type: 'Person',
|
type: 'Person',
|
||||||
preferredUsername: user.username,
|
preferredUsername: user.username,
|
||||||
name: userSettings.showRealName
|
name: userSettings.showRealName
|
||||||
|
@ -123,8 +130,8 @@ module.exports = class Actor extends ActivityPubObject {
|
||||||
const publicKeyPem = user.getProperty(UserProps.PublicActivityPubSigningKey);
|
const publicKeyPem = user.getProperty(UserProps.PublicActivityPubSigningKey);
|
||||||
if (!_.isEmpty(publicKeyPem)) {
|
if (!_.isEmpty(publicKeyPem)) {
|
||||||
obj.publicKey = {
|
obj.publicKey = {
|
||||||
id: userSelfUrl + '#main-key',
|
id: userActorId + '#main-key',
|
||||||
owner: userSelfUrl,
|
owner: userActorId,
|
||||||
publicKeyPem,
|
publicKeyPem,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -219,6 +226,7 @@ module.exports = class Actor extends ActivityPubObject {
|
||||||
|
|
||||||
const timestamp = moment(row.timestamp);
|
const timestamp = moment(row.timestamp);
|
||||||
if (moment().isAfter(timestamp.add(ActorCacheTTL))) {
|
if (moment().isAfter(timestamp.add(ActorCacheTTL))) {
|
||||||
|
// :TODO: drop from cache
|
||||||
return cb(Errors.Expired('The cache entry is expired'));
|
return cb(Errors.Expired('The cache entry is expired'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,11 +4,10 @@ const apDb = require('../database').dbs.activitypub;
|
||||||
const { getISOTimestampString } = require('../database');
|
const { getISOTimestampString } = require('../database');
|
||||||
const { Errors } = require('../enig_error.js');
|
const { Errors } = require('../enig_error.js');
|
||||||
const { PublicCollectionId: APPublicCollectionId } = require('./const');
|
const { PublicCollectionId: APPublicCollectionId } = require('./const');
|
||||||
|
const UserProps = require('../user_property');
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const { isString, get, isObject } = require('lodash');
|
const { isString } = require('lodash');
|
||||||
|
|
||||||
const APPublicOwningUserId = 0;
|
|
||||||
|
|
||||||
module.exports = class Collection extends ActivityPubObject {
|
module.exports = class Collection extends ActivityPubObject {
|
||||||
constructor(obj) {
|
constructor(obj) {
|
||||||
|
@ -19,34 +18,33 @@ module.exports = class Collection extends ActivityPubObject {
|
||||||
return APPublicCollectionId;
|
return APPublicCollectionId;
|
||||||
}
|
}
|
||||||
|
|
||||||
static followers(owningUser, page, webServer, cb) {
|
static followers(collectionId, page, cb) {
|
||||||
return Collection.getOrdered(
|
return Collection.publicOrderedById(
|
||||||
'followers',
|
'followers',
|
||||||
owningUser,
|
collectionId,
|
||||||
false,
|
|
||||||
page,
|
page,
|
||||||
e => e.id,
|
e => e.id,
|
||||||
webServer,
|
|
||||||
cb
|
cb
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static following(owningUser, page, webServer, cb) {
|
static following(collectionId, page, cb) {
|
||||||
return Collection.getOrdered(
|
return Collection.publicOrderedById(
|
||||||
'following',
|
'following',
|
||||||
owningUser,
|
collectionId,
|
||||||
false,
|
|
||||||
page,
|
page,
|
||||||
e => get(e, 'object.id'),
|
e => e.id,
|
||||||
webServer,
|
|
||||||
cb
|
cb
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static addFollower(owningUser, followingActor, cb) {
|
static addFollower(owningUser, followingActor, webServer, cb) {
|
||||||
|
const collectionId =
|
||||||
|
makeUserUrl(webServer, owningUser, '/ap/collections/') + '/followers';
|
||||||
return Collection.addToCollection(
|
return Collection.addToCollection(
|
||||||
'followers',
|
'followers',
|
||||||
owningUser,
|
owningUser,
|
||||||
|
collectionId,
|
||||||
followingActor.id,
|
followingActor.id,
|
||||||
followingActor,
|
followingActor,
|
||||||
false,
|
false,
|
||||||
|
@ -54,10 +52,13 @@ module.exports = class Collection extends ActivityPubObject {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static addFollowRequest(owningUser, requestingActor, cb) {
|
static addFollowRequest(owningUser, requestingActor, webServer, cb) {
|
||||||
|
const collectionId =
|
||||||
|
makeUserUrl(webServer, owningUser, '/ap/collections/') + '/follow-requests';
|
||||||
return Collection.addToCollection(
|
return Collection.addToCollection(
|
||||||
'follow_requests',
|
'follow-requests',
|
||||||
owningUser,
|
owningUser,
|
||||||
|
collectionId,
|
||||||
requestingActor.id,
|
requestingActor.id,
|
||||||
requestingActor,
|
requestingActor,
|
||||||
true,
|
true,
|
||||||
|
@ -65,22 +66,17 @@ module.exports = class Collection extends ActivityPubObject {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static outbox(owningUser, page, webServer, cb) {
|
static outbox(collectionId, page, cb) {
|
||||||
return Collection.getOrdered(
|
return Collection.publicOrderedById('outbox', collectionId, page, null, cb);
|
||||||
'outbox',
|
|
||||||
owningUser,
|
|
||||||
false,
|
|
||||||
page,
|
|
||||||
null,
|
|
||||||
webServer,
|
|
||||||
cb
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static addOutboxItem(owningUser, outboxItem, isPrivate, cb) {
|
static addOutboxItem(owningUser, outboxItem, isPrivate, webServer, cb) {
|
||||||
|
const collectionId =
|
||||||
|
makeUserUrl(webServer, owningUser, '/ap/collections/') + '/outbox';
|
||||||
return Collection.addToCollection(
|
return Collection.addToCollection(
|
||||||
'outbox',
|
'outbox',
|
||||||
owningUser,
|
owningUser,
|
||||||
|
collectionId,
|
||||||
outboxItem.id,
|
outboxItem.id,
|
||||||
outboxItem,
|
outboxItem,
|
||||||
isPrivate,
|
isPrivate,
|
||||||
|
@ -88,10 +84,13 @@ module.exports = class Collection extends ActivityPubObject {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static addInboxItem(inboxItem, owningUser, cb) {
|
static addInboxItem(inboxItem, owningUser, webServer, cb) {
|
||||||
|
const collectionId =
|
||||||
|
makeUserUrl(webServer, owningUser, '/ap/collections/') + '/inbox';
|
||||||
return Collection.addToCollection(
|
return Collection.addToCollection(
|
||||||
'inbox',
|
'inbox',
|
||||||
owningUser,
|
owningUser,
|
||||||
|
collectionId,
|
||||||
inboxItem.id,
|
inboxItem.id,
|
||||||
inboxItem,
|
inboxItem,
|
||||||
true,
|
true,
|
||||||
|
@ -102,7 +101,8 @@ module.exports = class Collection extends ActivityPubObject {
|
||||||
static addPublicInboxItem(inboxItem, cb) {
|
static addPublicInboxItem(inboxItem, cb) {
|
||||||
return Collection.addToCollection(
|
return Collection.addToCollection(
|
||||||
'publicInbox',
|
'publicInbox',
|
||||||
APPublicOwningUserId,
|
null, // N/A
|
||||||
|
Collection.PublicCollectionId,
|
||||||
inboxItem.id,
|
inboxItem.id,
|
||||||
inboxItem,
|
inboxItem,
|
||||||
false,
|
false,
|
||||||
|
@ -114,11 +114,11 @@ module.exports = class Collection extends ActivityPubObject {
|
||||||
const privateQuery = includePrivate ? '' : ' AND is_private = FALSE';
|
const privateQuery = includePrivate ? '' : ' AND is_private = FALSE';
|
||||||
|
|
||||||
apDb.get(
|
apDb.get(
|
||||||
`SELECT obj_json
|
`SELECT object_json
|
||||||
FROM collection
|
FROM collection
|
||||||
WHERE name = ?
|
WHERE name = ?
|
||||||
${privateQuery}
|
${privateQuery}
|
||||||
AND json_extract(obj_json, '$.object.id') = ?;`,
|
AND json_extract(object_json, '$.object.id') = ?;`,
|
||||||
[collectionName, objectId],
|
[collectionName, objectId],
|
||||||
(err, row) => {
|
(err, row) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
|
@ -133,7 +133,7 @@ module.exports = class Collection extends ActivityPubObject {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const obj = ActivityPubObject.fromJsonString(row.obj_json);
|
const obj = ActivityPubObject.fromJsonString(row.object_json);
|
||||||
if (!obj) {
|
if (!obj) {
|
||||||
return cb(Errors.Invalid('Failed to parse Object JSON'));
|
return cb(Errors.Invalid('Failed to parse Object JSON'));
|
||||||
}
|
}
|
||||||
|
@ -143,7 +143,71 @@ module.exports = class Collection extends ActivityPubObject {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static getOrdered(
|
static publicOrderedById(collectionName, collectionId, page, mapper, cb) {
|
||||||
|
if (!page) {
|
||||||
|
return apDb.get(
|
||||||
|
`SELECT COUNT(collection_id) AS count
|
||||||
|
FROM collection
|
||||||
|
WHERE name = ? AND collection_id = ? AND is_private = FALSE;`,
|
||||||
|
[collectionName, collectionId],
|
||||||
|
(err, row) => {
|
||||||
|
if (err) {
|
||||||
|
return cb(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
let obj;
|
||||||
|
if (row.count > 0) {
|
||||||
|
obj = {
|
||||||
|
id: collectionId,
|
||||||
|
type: 'OrderedCollection',
|
||||||
|
first: `${collectionId}?page=1`,
|
||||||
|
totalItems: row.count,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
obj = {
|
||||||
|
id: collectionId,
|
||||||
|
type: 'OrderedCollection',
|
||||||
|
totalItems: 0,
|
||||||
|
orderedItems: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return cb(null, new Collection(obj));
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// :TODO: actual paging...
|
||||||
|
apDb.all(
|
||||||
|
`SELECT object_json
|
||||||
|
FROM collection
|
||||||
|
WHERE name = ? AND collection_id = ? AND is_private = FALSE
|
||||||
|
ORDER BY timestamp;`,
|
||||||
|
[collectionName, collectionId],
|
||||||
|
(err, entries) => {
|
||||||
|
if (err) {
|
||||||
|
return cb(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
entries = entries || [];
|
||||||
|
if (mapper && entries.length > 0) {
|
||||||
|
entries = entries.map(mapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
const obj = {
|
||||||
|
id: `${collectionId}/page=${page}`,
|
||||||
|
type: 'OrderedCollectionPage',
|
||||||
|
totalItems: entries.length,
|
||||||
|
orderedItems: entries,
|
||||||
|
partOf: collectionId,
|
||||||
|
};
|
||||||
|
|
||||||
|
return cb(null, new Collection(obj));
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static ownedOrderedByUser(
|
||||||
collectionName,
|
collectionName,
|
||||||
owningUser,
|
owningUser,
|
||||||
includePrivate,
|
includePrivate,
|
||||||
|
@ -153,19 +217,25 @@ module.exports = class Collection extends ActivityPubObject {
|
||||||
cb
|
cb
|
||||||
) {
|
) {
|
||||||
const privateQuery = includePrivate ? '' : ' AND is_private = FALSE';
|
const privateQuery = includePrivate ? '' : ' AND is_private = FALSE';
|
||||||
const owningUserId = isObject(owningUser) ? owningUser.userId : owningUser;
|
const actorId = owningUser.getProperty(UserProps.ActivityPubActorId);
|
||||||
|
if (!actorId) {
|
||||||
|
return cb(
|
||||||
|
Errors.MissingProperty(
|
||||||
|
`User "${owningUser.username}" is missing property '${UserProps.ActivityPubActorId}'`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// e.g. http://some.host/_enig/ap/collections/1234/followers
|
// e.g. http://somewhere.com/_enig/ap/collections/NuSkooler/followers
|
||||||
const collectionIdBase =
|
const collectionId =
|
||||||
makeUserUrl(webServer, owningUser, `/ap/collections/${owningUserId}`) +
|
makeUserUrl(webServer, owningUser, '/ap/collections/') + `/${collectionName}`;
|
||||||
`/${collectionName}`;
|
|
||||||
|
|
||||||
if (!page) {
|
if (!page) {
|
||||||
return apDb.get(
|
return apDb.get(
|
||||||
`SELECT COUNT(id) AS count
|
`SELECT COUNT(collection_id) AS count
|
||||||
FROM collection
|
FROM collection
|
||||||
WHERE user_id = ? AND name = ?${privateQuery};`,
|
WHERE owner_actor_id = ? AND name = ?${privateQuery};`,
|
||||||
[owningUserId, collectionName],
|
[actorId, collectionName],
|
||||||
(err, row) => {
|
(err, row) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
|
@ -180,14 +250,14 @@ module.exports = class Collection extends ActivityPubObject {
|
||||||
let obj;
|
let obj;
|
||||||
if (row.count > 0) {
|
if (row.count > 0) {
|
||||||
obj = {
|
obj = {
|
||||||
id: collectionIdBase,
|
id: collectionId,
|
||||||
type: 'OrderedCollection',
|
type: 'OrderedCollection',
|
||||||
first: `${collectionIdBase}?page=1`,
|
first: `${collectionId}?page=1`,
|
||||||
totalItems: row.count,
|
totalItems: row.count,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
obj = {
|
obj = {
|
||||||
id: collectionIdBase,
|
id: collectionId,
|
||||||
type: 'OrderedCollection',
|
type: 'OrderedCollection',
|
||||||
totalItems: 0,
|
totalItems: 0,
|
||||||
orderedItems: [],
|
orderedItems: [],
|
||||||
|
@ -201,11 +271,11 @@ module.exports = class Collection extends ActivityPubObject {
|
||||||
|
|
||||||
// :TODO: actual paging...
|
// :TODO: actual paging...
|
||||||
apDb.all(
|
apDb.all(
|
||||||
`SELECT obj_json
|
`SELECT object_json
|
||||||
FROM collection
|
FROM collection
|
||||||
WHERE user_id = ? AND name = ?${privateQuery}
|
WHERE owner_actor_id = ? AND name = ?${privateQuery}
|
||||||
ORDER BY timestamp;`,
|
ORDER BY timestamp;`,
|
||||||
[owningUserId, collectionName],
|
[actorId, collectionName],
|
||||||
(err, entries) => {
|
(err, entries) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
|
@ -217,11 +287,11 @@ module.exports = class Collection extends ActivityPubObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
const obj = {
|
const obj = {
|
||||||
id: `${collectionIdBase}/page=${page}`,
|
id: `${collectionId}/page=${page}`,
|
||||||
type: 'OrderedCollectionPage',
|
type: 'OrderedCollectionPage',
|
||||||
totalItems: entries.length,
|
totalItems: entries.length,
|
||||||
orderedItems: entries,
|
orderedItems: entries,
|
||||||
partOf: collectionIdBase,
|
partOf: collectionId,
|
||||||
};
|
};
|
||||||
|
|
||||||
return cb(null, new Collection(obj));
|
return cb(null, new Collection(obj));
|
||||||
|
@ -239,8 +309,8 @@ module.exports = class Collection extends ActivityPubObject {
|
||||||
|
|
||||||
apDb.run(
|
apDb.run(
|
||||||
`UPDATE collection
|
`UPDATE collection
|
||||||
SET obj_json = ?, timestamp = ?
|
SET object_json = ?, timestamp = ?
|
||||||
WHERE name = ? AND obj_id = ?;`,
|
WHERE name = ? AND object_id = ?;`,
|
||||||
[obj, collectionName, getISOTimestampString(), objectId],
|
[obj, collectionName, getISOTimestampString(), objectId],
|
||||||
err => {
|
err => {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
|
@ -248,20 +318,43 @@ module.exports = class Collection extends ActivityPubObject {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static addToCollection(collectionName, owningUser, objectId, obj, isPrivate, cb) {
|
static addToCollection(
|
||||||
|
collectionName,
|
||||||
|
owningUser,
|
||||||
|
collectionId,
|
||||||
|
objectId,
|
||||||
|
obj,
|
||||||
|
isPrivate,
|
||||||
|
cb
|
||||||
|
) {
|
||||||
if (!isString(obj)) {
|
if (!isString(obj)) {
|
||||||
obj = JSON.stringify(obj);
|
obj = JSON.stringify(obj);
|
||||||
}
|
}
|
||||||
|
|
||||||
const owningUserId = isObject(owningUser) ? owningUser.userId : owningUser;
|
let actorId;
|
||||||
|
if (owningUser) {
|
||||||
|
actorId = owningUser.getProperty(UserProps.ActivityPubActorId);
|
||||||
|
if (!actorId) {
|
||||||
|
return cb(
|
||||||
|
Errors.MissingProperty(
|
||||||
|
`User "${owningUser.username}" is missing property '${UserProps.ActivityPubActorId}'`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
actorId = Collection.APPublicCollectionId;
|
||||||
|
}
|
||||||
|
|
||||||
isPrivate = isPrivate ? 1 : 0;
|
isPrivate = isPrivate ? 1 : 0;
|
||||||
|
|
||||||
apDb.run(
|
apDb.run(
|
||||||
`INSERT OR IGNORE INTO collection (name, timestamp, user_id, obj_id, obj_json, is_private)
|
`INSERT OR IGNORE INTO collection (name, timestamp, collection_id, owner_actor_id, object_id, object_json, is_private)
|
||||||
VALUES (?, ?, ?, ?, ?, ?);`,
|
VALUES (?, ?, ?, ?, ?, ?, ?);`,
|
||||||
[
|
[
|
||||||
collectionName,
|
collectionName,
|
||||||
getISOTimestampString(),
|
getISOTimestampString(),
|
||||||
owningUserId,
|
collectionId,
|
||||||
|
actorId,
|
||||||
objectId,
|
objectId,
|
||||||
obj,
|
obj,
|
||||||
isPrivate,
|
isPrivate,
|
||||||
|
@ -269,6 +362,9 @@ module.exports = class Collection extends ActivityPubObject {
|
||||||
function res(err) {
|
function res(err) {
|
||||||
// non-arrow for 'this' scope
|
// non-arrow for 'this' scope
|
||||||
if (err) {
|
if (err) {
|
||||||
|
if ('SQLITE_CONSTRAINT' === err.code) {
|
||||||
|
err = null; // ignore
|
||||||
|
}
|
||||||
return cb(err);
|
return cb(err);
|
||||||
}
|
}
|
||||||
return cb(err, this.lastID);
|
return cb(err, this.lastID);
|
||||||
|
@ -277,11 +373,18 @@ module.exports = class Collection extends ActivityPubObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
static removeFromCollectionById(collectionName, owningUser, objectId, cb) {
|
static removeFromCollectionById(collectionName, owningUser, objectId, cb) {
|
||||||
const owningUserId = isObject(owningUser) ? owningUser.userId : owningUser;
|
const actorId = owningUser.getProperty(UserProps.ActivityPubActorId);
|
||||||
|
if (!actorId) {
|
||||||
|
return cb(
|
||||||
|
Errors.MissingProperty(
|
||||||
|
`User "${owningUser.username}" is missing property '${UserProps.ActivityPubActorId}'`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
apDb.run(
|
apDb.run(
|
||||||
`DELETE FROM collection
|
`DELETE FROM collection
|
||||||
WHERE user_id = ? AND name = ? AND obj_id = ?;`,
|
WHERE name = ? AND owner_actor_id = ? AND object_id = ?;`,
|
||||||
[owningUserId, collectionName, objectId],
|
[collectionName, actorId, objectId],
|
||||||
err => {
|
err => {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ const { getISOTimestampString } = require('../database');
|
||||||
const User = require('../user');
|
const User = require('../user');
|
||||||
const { messageBodyToHtml, htmlToMessageBody } = require('./util');
|
const { messageBodyToHtml, htmlToMessageBody } = require('./util');
|
||||||
const { isAnsi } = require('../string_util');
|
const { isAnsi } = require('../string_util');
|
||||||
|
const Log = require('../logger').log;
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const { v5: UUIDv5 } = require('uuid');
|
const { v5: UUIDv5 } = require('uuid');
|
||||||
|
@ -187,7 +188,10 @@ module.exports = class Note extends ActivityPubObject {
|
||||||
try {
|
try {
|
||||||
message.modTimestamp = moment(this.published);
|
message.modTimestamp = moment(this.published);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// :TODO: Log warning
|
Log.warn(
|
||||||
|
{ published: this.published, error: e.message },
|
||||||
|
'Failed to parse Note published timestamp'
|
||||||
|
);
|
||||||
message.modTimestamp = moment();
|
message.modTimestamp = moment();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -203,9 +207,30 @@ module.exports = class Note extends ActivityPubObject {
|
||||||
if (this.inReplyTo) {
|
if (this.inReplyTo) {
|
||||||
message.meta.ActivityPub[Message.ActivityPubPropertyNames.InReplyTo] =
|
message.meta.ActivityPub[Message.ActivityPubPropertyNames.InReplyTo] =
|
||||||
this.inReplyTo;
|
this.inReplyTo;
|
||||||
}
|
|
||||||
|
|
||||||
return cb(null, message);
|
const filter = {
|
||||||
|
resultType: 'id',
|
||||||
|
metaTuples: [
|
||||||
|
{
|
||||||
|
category: Message.WellKnownMetaCategories.ActivityPub,
|
||||||
|
name: Message.ActivityPubPropertyNames.InReplyTo,
|
||||||
|
value: this.inReplyTo,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
limit: 1,
|
||||||
|
};
|
||||||
|
Message.findMessages(filter, (err, messageId) => {
|
||||||
|
if (messageId) {
|
||||||
|
// we get an array, but limited 1; use the first
|
||||||
|
messageId = messageId[0];
|
||||||
|
message.replyToMsgId = messageId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return cb(null, message);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return cb(null, message);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,14 +1,18 @@
|
||||||
const UserProps = require('../user_property');
|
const UserProps = require('../user_property');
|
||||||
|
const Config = require('../config').get;
|
||||||
|
|
||||||
module.exports = class ActivityPubSettings {
|
module.exports = class ActivityPubSettings {
|
||||||
constructor(obj) {
|
constructor(obj) {
|
||||||
this.enabled = true; // :TODO: fetch from +op config default
|
this.enabled = true;
|
||||||
this.manuallyApproveFollowers = false;
|
this.manuallyApproveFollowers = false;
|
||||||
this.hideSocialGraph = false; // followers, following
|
this.hideSocialGraph = false; // followers, following
|
||||||
this.showRealName = true;
|
this.showRealName = true;
|
||||||
this.image = '';
|
this.image = '';
|
||||||
this.icon = '';
|
this.icon = '';
|
||||||
|
|
||||||
|
// override default with any op config
|
||||||
|
Object.assign(this, Config().users.activityPub);
|
||||||
|
|
||||||
if (obj) {
|
if (obj) {
|
||||||
Object.assign(this, obj);
|
Object.assign(this, obj);
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,9 +20,8 @@ exports.isValidLink = isValidLink;
|
||||||
exports.makeSharedInboxUrl = makeSharedInboxUrl;
|
exports.makeSharedInboxUrl = makeSharedInboxUrl;
|
||||||
exports.makeUserUrl = makeUserUrl;
|
exports.makeUserUrl = makeUserUrl;
|
||||||
exports.webFingerProfileUrl = webFingerProfileUrl;
|
exports.webFingerProfileUrl = webFingerProfileUrl;
|
||||||
exports.selfUrl = selfUrl;
|
exports.localActorId = localActorId;
|
||||||
exports.userFromAccount = userFromAccount;
|
exports.userFromActorId = userFromActorId;
|
||||||
exports.accountFromSelfUrl = accountFromSelfUrl;
|
|
||||||
exports.getUserProfileTemplatedBody = getUserProfileTemplatedBody;
|
exports.getUserProfileTemplatedBody = getUserProfileTemplatedBody;
|
||||||
exports.messageBodyToHtml = messageBodyToHtml;
|
exports.messageBodyToHtml = messageBodyToHtml;
|
||||||
exports.htmlToMessageBody = htmlToMessageBody;
|
exports.htmlToMessageBody = htmlToMessageBody;
|
||||||
|
@ -59,26 +58,26 @@ function webFingerProfileUrl(webServer, user) {
|
||||||
return webServer.buildUrl(WellKnownLocations.Internal + `/wf/@${user.username}`);
|
return webServer.buildUrl(WellKnownLocations.Internal + `/wf/@${user.username}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function selfUrl(webServer, user) {
|
function localActorId(webServer, user) {
|
||||||
return makeUserUrl(webServer, user, '/ap/users/');
|
return makeUserUrl(webServer, user, '/ap/users/');
|
||||||
}
|
}
|
||||||
|
|
||||||
function accountFromSelfUrl(url) {
|
function userFromActorId(actorId, cb) {
|
||||||
// https://some.l33t.enigma.board/_enig/ap/users/Masto -> Masto
|
User.getUserIdsWithProperty(UserProps.ActivityPubActorId, actorId, (err, userId) => {
|
||||||
// :TODO: take webServer, and just take path-to-users.length +1
|
|
||||||
return url.substring(url.lastIndexOf('/') + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
function userFromAccount(accountName, cb) {
|
|
||||||
if (accountName.startsWith('@')) {
|
|
||||||
accountName = accountName.slice(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
User.getUserIdAndName(accountName, (err, userId) => {
|
|
||||||
if (err) {
|
if (err) {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// must only be 0 or 1
|
||||||
|
if (!Array.isArray(userId) || userId.length !== 1) {
|
||||||
|
return cb(
|
||||||
|
Errors.DoesNotExist(
|
||||||
|
`No user with property '${UserProps.ActivityPubActorId}' of ${actorId}`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
userId = userId[0];
|
||||||
User.getUser(userId, (err, user) => {
|
User.getUser(userId, (err, user) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
|
|
|
@ -136,6 +136,14 @@ module.exports = () => {
|
||||||
storagePath: paths.join(__dirname, '../userdata/avatars/'),
|
storagePath: paths.join(__dirname, '../userdata/avatars/'),
|
||||||
spritesPath: paths.join(__dirname, '../misc/avatar-sprites/'),
|
spritesPath: paths.join(__dirname, '../misc/avatar-sprites/'),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// See also ./core/activitypub/settings.js
|
||||||
|
activityPub: {
|
||||||
|
enabled: true, // ActivityPub enabled for this user?
|
||||||
|
manuallyApproveFollowers: false,
|
||||||
|
hideSocialGraph: false,
|
||||||
|
showRealName: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
theme: {
|
theme: {
|
||||||
|
|
|
@ -505,7 +505,6 @@ dbs.message.run(
|
||||||
// Actors we know about and have cached
|
// Actors we know about and have cached
|
||||||
dbs.activitypub.run(
|
dbs.activitypub.run(
|
||||||
`CREATE TABLE IF NOT EXISTS actor_cache (
|
`CREATE TABLE IF NOT EXISTS actor_cache (
|
||||||
actor_cache_id INTEGER PRIMARY KEY, -- Local DB ID
|
|
||||||
actor_id VARCHAR NOT NULL, -- Fully qualified Actor ID/URL
|
actor_id VARCHAR NOT NULL, -- Fully qualified Actor ID/URL
|
||||||
actor_json VARCHAR NOT NULL, -- Actor document
|
actor_json VARCHAR NOT NULL, -- Actor document
|
||||||
subject VARCHAR, -- Subject obtained from WebFinger, e.g. @Username@some.domain
|
subject VARCHAR, -- Subject obtained from WebFinger, e.g. @Username@some.domain
|
||||||
|
@ -524,32 +523,36 @@ dbs.message.run(
|
||||||
// generally obtained via WebFinger
|
// generally obtained via WebFinger
|
||||||
dbs.activitypub.run(
|
dbs.activitypub.run(
|
||||||
`CREATE TABLE IF NOT EXISTS actor_alias_cache (
|
`CREATE TABLE IF NOT EXISTS actor_alias_cache (
|
||||||
id INTEGER PRIMARY KEY,
|
|
||||||
alias VARCHAR NOT NULL,
|
|
||||||
actor_id VARCHAR NOT NULL, -- Fully qualified Actor ID/URL
|
actor_id VARCHAR NOT NULL, -- Fully qualified Actor ID/URL
|
||||||
|
actor_alias_id VARCHAR NOT NULL, -- Alias such the user's "profile URL"
|
||||||
|
|
||||||
UNIQUE(alias)
|
UNIQUE(actor_alias_id)
|
||||||
);`
|
);`
|
||||||
);
|
);
|
||||||
|
|
||||||
// ActivityPub Collections of various types such as followers, following, likes, ...
|
// ActivityPub Collections of various types such as followers, following, likes, ...
|
||||||
dbs.activitypub.run(
|
dbs.activitypub.run(
|
||||||
`CREATE TABLE IF NOT EXISTS collection (
|
`CREATE TABLE IF NOT EXISTS collection (
|
||||||
id INTEGER PRIMARY KEY, -- Auto-generated key
|
collection_id VARCHAR NOT NULL, -- ie: http://somewhere.com/_enig/ap/collections/NuSkooler/followers
|
||||||
name VARCHAR NOT NULL, -- examples: followers, follows, ...
|
name VARCHAR NOT NULL, -- examples: followers, follows, ...
|
||||||
timestamp DATETIME NOT NULL, -- Timestamp in which this entry was created
|
timestamp DATETIME NOT NULL, -- Timestamp in which this entry was created
|
||||||
user_id INTEGER NOT NULL, -- Local, owning user ID, 0 means "all" for sharedInbox
|
owner_actor_id VARCHAR NOT NULL, -- Local, owning Actor ID, or the #Public magic collection ID
|
||||||
obj_id VARCHAR NOT NULL, -- Object ID from obj_json.id
|
object_id VARCHAR NOT NULL, -- Object ID from obj_json.id
|
||||||
obj_json VARCHAR NOT NULL, -- Object varies by collection (obj_json.type)
|
object_json VARCHAR NOT NULL, -- Object varies by collection (obj_json.type)
|
||||||
is_private INTEGER NOT NULL, -- Is this object private to |user_id|?
|
is_private INTEGER NOT NULL, -- Is this object private to |owner_actor_id|?
|
||||||
|
|
||||||
UNIQUE(name, user_id, obj_id)
|
UNIQUE(name, collection_id, object_id)
|
||||||
);`
|
);`
|
||||||
);
|
);
|
||||||
|
|
||||||
dbs.activitypub.run(
|
dbs.activitypub.run(
|
||||||
`CREATE INDEX IF NOT EXISTS collection_entry_by_user_index0
|
`CREATE INDEX IF NOT EXISTS collection_entry_by_name_actor_id_index0
|
||||||
ON collection (name, user_id);`
|
ON collection (name, owner_actor_id);`
|
||||||
|
);
|
||||||
|
|
||||||
|
dbs.activitypub.run(
|
||||||
|
`CREATE INDEX IF NOT EXISTS collection_entry_by_name_collection_id_index0
|
||||||
|
ON collection (name, collection_id);`
|
||||||
);
|
);
|
||||||
|
|
||||||
return cb(null);
|
return cb(null);
|
||||||
|
|
|
@ -99,6 +99,7 @@ exports.getModule = class ActivityPubScannerTosser extends MessageScanTossModule
|
||||||
fromUser,
|
fromUser,
|
||||||
activity,
|
activity,
|
||||||
message.isPrivate(),
|
message.isPrivate(),
|
||||||
|
this._webServer(),
|
||||||
(err, localId) => {
|
(err, localId) => {
|
||||||
if (!err) {
|
if (!err) {
|
||||||
this.log.debug(
|
this.log.debug(
|
||||||
|
|
|
@ -89,7 +89,7 @@ exports.getModule = class WebServerModule extends ServerModule {
|
||||||
|
|
||||||
getDomain() {
|
getDomain() {
|
||||||
const config = Config();
|
const config = Config();
|
||||||
const overridePrefix = _.get(config.contentServers.web.overrideUrlPrefix);
|
const overridePrefix = _.get(config, 'contentServers.web.overrideUrlPrefix');
|
||||||
if (_.isString(overridePrefix)) {
|
if (_.isString(overridePrefix)) {
|
||||||
const url = new URL(overridePrefix);
|
const url = new URL(overridePrefix);
|
||||||
return url.hostname;
|
return url.hostname;
|
||||||
|
@ -98,17 +98,11 @@ exports.getModule = class WebServerModule extends ServerModule {
|
||||||
return config.contentServers.web.domain;
|
return config.contentServers.web.domain;
|
||||||
}
|
}
|
||||||
|
|
||||||
buildUrl(pathAndQuery) {
|
baseUrl() {
|
||||||
//
|
|
||||||
// Create a URL such as
|
|
||||||
// https://l33t.codes:44512/ + |pathAndQuery|
|
|
||||||
//
|
|
||||||
// Prefer HTTPS over HTTP. Be explicit about the port
|
|
||||||
// only if non-standard. Allow users to override full prefix in config.
|
|
||||||
//
|
|
||||||
const config = Config();
|
const config = Config();
|
||||||
if (_.isString(config.contentServers.web.overrideUrlPrefix)) {
|
const overridePrefix = _.get(config, 'contentServers.web.overrideUrlPrefix');
|
||||||
return `${config.contentServers.web.overrideUrlPrefix}${pathAndQuery}`;
|
if (overridePrefix) {
|
||||||
|
return overridePrefix;
|
||||||
}
|
}
|
||||||
|
|
||||||
let schema;
|
let schema;
|
||||||
|
@ -127,7 +121,24 @@ exports.getModule = class WebServerModule extends ServerModule {
|
||||||
: `:${config.contentServers.web.http.port}`;
|
: `:${config.contentServers.web.http.port}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return `${schema}${config.contentServers.web.domain}${port}${pathAndQuery}`;
|
return `${schema}${config.contentServers.web.domain}${port}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
fullUrl(req) {
|
||||||
|
const base = this.baseUrl();
|
||||||
|
return new URL(`${base}${req.url}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
buildUrl(pathAndQuery) {
|
||||||
|
//
|
||||||
|
// Create a URL such as
|
||||||
|
// https://l33t.codes:44512/ + |pathAndQuery|
|
||||||
|
//
|
||||||
|
// Prefer HTTPS over HTTP. Be explicit about the port
|
||||||
|
// only if non-standard. Allow users to override full prefix in config.
|
||||||
|
//
|
||||||
|
const base = this.baseUrl();
|
||||||
|
return `${base}${pathAndQuery}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
isEnabled() {
|
isEnabled() {
|
||||||
|
|
|
@ -1,25 +1,28 @@
|
||||||
const WebHandlerModule = require('../../../web_handler_module');
|
const WebHandlerModule = require('../../../web_handler_module');
|
||||||
const {
|
const {
|
||||||
userFromAccount,
|
userFromActorId,
|
||||||
getUserProfileTemplatedBody,
|
getUserProfileTemplatedBody,
|
||||||
DefaultProfileTemplate,
|
DefaultProfileTemplate,
|
||||||
accountFromSelfUrl,
|
makeUserUrl,
|
||||||
|
localActorId,
|
||||||
} = require('../../../activitypub/util');
|
} = require('../../../activitypub/util');
|
||||||
const Config = require('../../../config').get;
|
const Config = require('../../../config').get;
|
||||||
const Activity = require('../../../activitypub/activity');
|
const Activity = require('../../../activitypub/activity');
|
||||||
const ActivityPubSettings = require('../../../activitypub/settings');
|
const ActivityPubSettings = require('../../../activitypub/settings');
|
||||||
const Actor = require('../../../activitypub/actor');
|
const Actor = require('../../../activitypub/actor');
|
||||||
const Collection = require('../../../activitypub/collection');
|
const Collection = require('../../../activitypub/collection');
|
||||||
|
const Note = require('../../../activitypub/note');
|
||||||
const EnigAssert = require('../../../enigma_assert');
|
const EnigAssert = require('../../../enigma_assert');
|
||||||
const Message = require('../../../message');
|
const Message = require('../../../message');
|
||||||
|
const Events = require('../../../events');
|
||||||
|
const UserProps = require('../../../user_property');
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const enigma_assert = require('../../../enigma_assert');
|
const enigma_assert = require('../../../enigma_assert');
|
||||||
const httpSignature = require('http-signature');
|
const httpSignature = require('http-signature');
|
||||||
const async = require('async');
|
const async = require('async');
|
||||||
const Note = require('../../../activitypub/note');
|
const paths = require('path');
|
||||||
const User = require('../../../user');
|
|
||||||
|
|
||||||
exports.moduleInfo = {
|
exports.moduleInfo = {
|
||||||
name: 'ActivityPub',
|
name: 'ActivityPub',
|
||||||
|
@ -41,6 +44,11 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
|
||||||
|
|
||||||
this.log = webServer.logger().child({ webHandler: 'ActivityPub' });
|
this.log = webServer.logger().child({ webHandler: 'ActivityPub' });
|
||||||
|
|
||||||
|
Events.addListener(Events.getSystemEvents().NewUserPrePersist, eventInfo => {
|
||||||
|
const { user, callback } = eventInfo;
|
||||||
|
return this._prepareNewUserAsActor(user, callback);
|
||||||
|
});
|
||||||
|
|
||||||
this.webServer.addRoute({
|
this.webServer.addRoute({
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
path: /^\/_enig\/ap\/users\/[^/]+$/,
|
path: /^\/_enig\/ap\/users\/[^/]+$/,
|
||||||
|
@ -131,40 +139,37 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
|
||||||
_selfUrlRequestHandler(req, resp) {
|
_selfUrlRequestHandler(req, resp) {
|
||||||
this.log.trace({ url: req.url }, 'Request for "self"');
|
this.log.trace({ url: req.url }, 'Request for "self"');
|
||||||
|
|
||||||
const url = new URL(req.url, `https://${req.headers.host}`);
|
let actorId = this.webServer.fullUrl(req).toString();
|
||||||
let accountName = url.pathname.substring(url.pathname.lastIndexOf('/') + 1);
|
|
||||||
let sendActor = false;
|
let sendActor = false;
|
||||||
|
if (actorId.endsWith('.json')) {
|
||||||
// Like Mastodon, if .json is appended onto URL then return the JSON content
|
|
||||||
if (accountName.endsWith('.json')) {
|
|
||||||
sendActor = true;
|
sendActor = true;
|
||||||
accountName = accountName.slice(0, -5);
|
actorId = actorId.slice(0, -5);
|
||||||
}
|
}
|
||||||
|
|
||||||
userFromAccount(accountName, (err, user) => {
|
// Additionally, serve activity JSON if the proper 'Accept' header was sent
|
||||||
|
const accept = req.headers['accept'].split(',').map(v => v.trim()) || ['*/*'];
|
||||||
|
const headerValues = [
|
||||||
|
ActivityJsonMime,
|
||||||
|
'application/ld+json',
|
||||||
|
'application/json',
|
||||||
|
];
|
||||||
|
if (accept.some(v => headerValues.includes(v))) {
|
||||||
|
sendActor = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
userFromActorId(actorId, (err, localUser) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
this.log.info(
|
this.log.info(
|
||||||
{ reason: err.message, accountName: accountName },
|
{ error: err.message, actorId },
|
||||||
`No user "${accountName}" for "self"`
|
`No user for Actor ID ${actorId}`
|
||||||
);
|
);
|
||||||
return this.webServer.resourceNotFound(resp);
|
return this.webServer.resourceNotFound(resp);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Additionally, serve activity JSON if the proper 'Accept' header was sent
|
|
||||||
const accept = req.headers['accept'].split(',').map(v => v.trim()) || ['*/*'];
|
|
||||||
const headerValues = [
|
|
||||||
ActivityJsonMime,
|
|
||||||
'application/ld+json',
|
|
||||||
'application/json',
|
|
||||||
];
|
|
||||||
if (accept.some(v => headerValues.includes(v))) {
|
|
||||||
sendActor = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sendActor) {
|
if (sendActor) {
|
||||||
return this._selfAsActorHandler(user, req, resp);
|
return this._selfAsActorHandler(localUser, req, resp);
|
||||||
} else {
|
} else {
|
||||||
return this._standardSelfHandler(user, req, resp);
|
return this._standardSelfHandler(localUser, req, resp);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -197,8 +202,9 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
|
||||||
|
|
||||||
switch (activity.type) {
|
switch (activity.type) {
|
||||||
case 'Follow':
|
case 'Follow':
|
||||||
return this._withUserRequestHandler(
|
return this._collectionRequestHandler(
|
||||||
signature,
|
signature,
|
||||||
|
'inbox',
|
||||||
activity,
|
activity,
|
||||||
this._inboxFollowRequestHandler.bind(this),
|
this._inboxFollowRequestHandler.bind(this),
|
||||||
req,
|
req,
|
||||||
|
@ -209,9 +215,14 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
|
||||||
return this._inboxUpdateRequestHandler(activity, req, resp);
|
return this._inboxUpdateRequestHandler(activity, req, resp);
|
||||||
|
|
||||||
case 'Undo':
|
case 'Undo':
|
||||||
return this._inboxUndoRequestHandler(activity, req, resp);
|
return this._collectionRequestHandler(
|
||||||
|
signature,
|
||||||
// :TODO: Create, etc.
|
'inbox',
|
||||||
|
activity,
|
||||||
|
this._inboxUndoRequestHandler.bind(this),
|
||||||
|
req,
|
||||||
|
resp
|
||||||
|
);
|
||||||
|
|
||||||
default:
|
default:
|
||||||
this.log.warn(
|
this.log.warn(
|
||||||
|
@ -264,15 +275,19 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
|
||||||
}
|
}
|
||||||
|
|
||||||
_sharedInboxCreateActivity(req, resp, activity) {
|
_sharedInboxCreateActivity(req, resp, activity) {
|
||||||
let toActors = activity.to;
|
const deliverTo = activity.recipientIds();
|
||||||
if (!Array.isArray(toActors)) {
|
|
||||||
toActors = [toActors];
|
//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');
|
const createWhat = _.get(activity, 'object.type');
|
||||||
switch (createWhat) {
|
switch (createWhat) {
|
||||||
case 'Note':
|
case 'Note':
|
||||||
return this._deliverSharedInboxNote(req, resp, toActors, activity);
|
return this._deliverSharedInboxNote(req, resp, deliverTo, activity);
|
||||||
|
|
||||||
default:
|
default:
|
||||||
this.log.warn(
|
this.log.warn(
|
||||||
|
@ -283,7 +298,7 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_deliverSharedInboxNote(req, resp, toActors, activity) {
|
_deliverSharedInboxNote(req, resp, deliverTo, activity) {
|
||||||
// When an object is being delivered to the originating actor's followers,
|
// When an object is being delivered to the originating actor's followers,
|
||||||
// a server MAY reduce the number of receiving actors delivered to by
|
// a server MAY reduce the number of receiving actors delivered to by
|
||||||
// identifying all followers which share the same sharedInbox who would
|
// identifying all followers which share the same sharedInbox who would
|
||||||
|
@ -297,26 +312,32 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
|
||||||
}
|
}
|
||||||
|
|
||||||
async.forEach(
|
async.forEach(
|
||||||
toActors,
|
deliverTo,
|
||||||
(actorId, nextActor) => {
|
(actorId, nextActor) => {
|
||||||
if (Collection.PublicCollectionId === actorId) {
|
switch (actorId) {
|
||||||
// :TODO: we should probably land this in a public areaTag as well for AP; allowing Message objects to be used/etc.
|
case Collection.PublicCollectionId:
|
||||||
Collection.addPublicInboxItem(note, err => {
|
// :TODO: we should probably land this in a public areaTag as well for AP; allowing Message objects to be used/etc.
|
||||||
return nextActor(err);
|
Collection.addPublicInboxItem(note, err => {
|
||||||
});
|
return nextActor(err);
|
||||||
} else {
|
});
|
||||||
this._deliverInboxNoteToLocalActor(
|
break;
|
||||||
req,
|
|
||||||
resp,
|
default:
|
||||||
actorId,
|
this._deliverInboxNoteToLocalActor(
|
||||||
activity,
|
req,
|
||||||
note,
|
resp,
|
||||||
nextActor
|
actorId,
|
||||||
);
|
activity,
|
||||||
|
note,
|
||||||
|
err => {
|
||||||
|
return nextActor(err);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
err => {
|
err => {
|
||||||
if (err && 'SQLITE_CONSTRAINT' !== err.code) {
|
if (err) {
|
||||||
return this.webServer.internalServerError(resp, err);
|
return this.webServer.internalServerError(resp, err);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -326,22 +347,12 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
|
||||||
}
|
}
|
||||||
|
|
||||||
_deliverInboxNoteToLocalActor(req, resp, actorId, activity, note, cb) {
|
_deliverInboxNoteToLocalActor(req, resp, actorId, activity, note, cb) {
|
||||||
const localUserName = accountFromSelfUrl(actorId);
|
userFromActorId(actorId, (err, localUser) => {
|
||||||
if (!localUserName) {
|
|
||||||
this.log.debug({ url: req.url }, 'Could not get username from URL');
|
|
||||||
return cb(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
User.getUserByUsername(localUserName, (err, localUser) => {
|
|
||||||
if (err) {
|
if (err) {
|
||||||
this.log.info(
|
return cb(null); // not found/etc., just bail
|
||||||
{ username: localUserName },
|
|
||||||
`No local user account for "${localUserName}"`
|
|
||||||
);
|
|
||||||
return cb(null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Collection.addInboxItem(note, localUser, err => {
|
Collection.addInboxItem(note, localUser, this.webServer, err => {
|
||||||
if (err) {
|
if (err) {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
}
|
}
|
||||||
|
@ -362,6 +373,17 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
|
||||||
}
|
}
|
||||||
|
|
||||||
message.persist(err => {
|
message.persist(err => {
|
||||||
|
if (!err) {
|
||||||
|
this.log.info(
|
||||||
|
{
|
||||||
|
user: localUser.username,
|
||||||
|
userId: localUser.userId,
|
||||||
|
activityId: activity.id,
|
||||||
|
noteId: note.id,
|
||||||
|
},
|
||||||
|
'Note delivered as message to private mailbox'
|
||||||
|
);
|
||||||
|
}
|
||||||
return cb(err);
|
return cb(err);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -369,45 +391,30 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_getCollectionHandler(name, req, resp, signature) {
|
_getCollectionHandler(collectionName, req, resp, signature) {
|
||||||
EnigAssert(signature, 'Missing signature!');
|
EnigAssert(signature, 'Missing signature!');
|
||||||
|
|
||||||
const url = new URL(req.url, `https://${req.headers.host}`);
|
const getCollection = Collection[collectionName];
|
||||||
const accountName = this._accountNameFromUserPath(url, name);
|
if (!getCollection) {
|
||||||
if (!accountName) {
|
|
||||||
return this.webServer.resourceNotFound(resp);
|
return this.webServer.resourceNotFound(resp);
|
||||||
}
|
}
|
||||||
|
|
||||||
// can we even handle this request?
|
const url = this.webServer.fullUrl(req);
|
||||||
const getter = Collection[name];
|
const page = url.searchParams.get('page');
|
||||||
if (!getter) {
|
const collectionId = url.toString();
|
||||||
return this.webServer.resourceNotFound(resp);
|
getCollection(collectionId, page, (err, collection) => {
|
||||||
}
|
|
||||||
|
|
||||||
userFromAccount(accountName, (err, user) => {
|
|
||||||
if (err) {
|
if (err) {
|
||||||
this.log.info(
|
return this.webServer.internalServerError(resp, err);
|
||||||
{ reason: err.message, accountName: accountName },
|
|
||||||
`No user "${accountName}" for "${name}"`
|
|
||||||
);
|
|
||||||
return this.webServer.resourceNotFound(resp);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const page = url.searchParams.get('page');
|
const body = JSON.stringify(collection);
|
||||||
getter(user, page, this.webServer, (err, collection) => {
|
const headers = {
|
||||||
if (err) {
|
'Content-Type': ActivityJsonMime,
|
||||||
return this.webServer.internalServerError(resp, err);
|
'Content-Length': body.length,
|
||||||
}
|
};
|
||||||
|
|
||||||
const body = JSON.stringify(collection);
|
resp.writeHead(200, headers);
|
||||||
const headers = {
|
return resp.end(body);
|
||||||
'Content-Type': ActivityJsonMime,
|
|
||||||
'Content-Length': body.length,
|
|
||||||
};
|
|
||||||
|
|
||||||
resp.writeHead(200, headers);
|
|
||||||
return resp.end(body);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -416,6 +423,11 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
|
||||||
return this._getCollectionHandler('following', req, resp, signature);
|
return this._getCollectionHandler('following', req, resp, signature);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_followersGetHandler(req, resp, signature) {
|
||||||
|
this.log.debug({ url: req.url }, 'Request for "followers"');
|
||||||
|
return this._getCollectionHandler('followers', req, resp, signature);
|
||||||
|
}
|
||||||
|
|
||||||
// https://docs.gotosocial.org/en/latest/federation/behaviors/outbox/
|
// https://docs.gotosocial.org/en/latest/federation/behaviors/outbox/
|
||||||
_outboxGetHandler(req, resp, signature) {
|
_outboxGetHandler(req, resp, signature) {
|
||||||
this.log.debug({ url: req.url }, 'Request for "outbox"');
|
this.log.debug({ url: req.url }, 'Request for "outbox"');
|
||||||
|
@ -425,9 +437,7 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
|
||||||
_singlePublicNoteGetHandler(req, resp) {
|
_singlePublicNoteGetHandler(req, resp) {
|
||||||
this.log.debug({ url: req.url }, 'Request for "Note"');
|
this.log.debug({ url: req.url }, 'Request for "Note"');
|
||||||
|
|
||||||
const url = new URL(req.url, `https://${req.headers.host}`);
|
const noteId = this.webServer.fullUrl(req).toString();
|
||||||
const noteId = url.toString();
|
|
||||||
|
|
||||||
Note.fromPublicNoteId(noteId, (err, note) => {
|
Note.fromPublicNoteId(noteId, (err, note) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
return this.webServer.internalServerError(resp, err);
|
return this.webServer.internalServerError(resp, err);
|
||||||
|
@ -449,11 +459,6 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
|
||||||
return m[1];
|
return m[1];
|
||||||
}
|
}
|
||||||
|
|
||||||
_followersGetHandler(req, resp, signature) {
|
|
||||||
this.log.debug({ url: req.url }, 'Request for "followers"');
|
|
||||||
return this._getCollectionHandler('followers', req, resp, signature);
|
|
||||||
}
|
|
||||||
|
|
||||||
_parseAndValidateSignature(req) {
|
_parseAndValidateSignature(req) {
|
||||||
let signature;
|
let signature;
|
||||||
try {
|
try {
|
||||||
|
@ -485,8 +490,11 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
|
||||||
return keyId.endsWith('#main-key');
|
return keyId.endsWith('#main-key');
|
||||||
}
|
}
|
||||||
|
|
||||||
_inboxFollowRequestHandler(activity, remoteActor, user, resp) {
|
_inboxFollowRequestHandler(activity, remoteActor, localUser, resp) {
|
||||||
this.log.info({ user_id: user.userId, actor: activity.actor }, 'Follow request');
|
this.log.info(
|
||||||
|
{ user_id: localUser.userId, actor: activity.actor },
|
||||||
|
'Follow request'
|
||||||
|
);
|
||||||
|
|
||||||
const ok = () => {
|
const ok = () => {
|
||||||
resp.writeHead(200, { 'Content-Type': 'text/html' });
|
resp.writeHead(200, { 'Content-Type': 'text/html' });
|
||||||
|
@ -499,12 +507,12 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
|
||||||
// request for the user to review and decide what to do with
|
// request for the user to review and decide what to do with
|
||||||
// at a later time.
|
// at a later time.
|
||||||
//
|
//
|
||||||
const activityPubSettings = ActivityPubSettings.fromUser(user);
|
const activityPubSettings = ActivityPubSettings.fromUser(localUser);
|
||||||
if (!activityPubSettings.manuallyApproveFollowers) {
|
if (!activityPubSettings.manuallyApproveFollowers) {
|
||||||
this._recordAcceptedFollowRequest(user, remoteActor, activity);
|
this._recordAcceptedFollowRequest(localUser, remoteActor, activity);
|
||||||
return ok();
|
return ok();
|
||||||
} else {
|
} else {
|
||||||
Collection.addFollowRequest(user, remoteActor, err => {
|
Collection.addFollowRequest(localUser, remoteActor, this.webServer, err => {
|
||||||
if (err) {
|
if (err) {
|
||||||
return this.internalServerError(resp, err);
|
return this.internalServerError(resp, err);
|
||||||
}
|
}
|
||||||
|
@ -523,55 +531,56 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
|
||||||
// });
|
// });
|
||||||
}
|
}
|
||||||
|
|
||||||
_inboxUndoRequestHandler(activity, req, resp) {
|
_inboxUndoRequestHandler(activity, remoteActor, localUser, resp) {
|
||||||
this.log.info({ actor: activity.actor }, 'Undo Activity request');
|
this.log.info(
|
||||||
|
{ user: localUser.username, actor: remoteActor.id },
|
||||||
|
'Undo Activity request'
|
||||||
|
);
|
||||||
|
|
||||||
const url = new URL(req.url, `https://${req.headers.host}`);
|
// we only understand Follow right now
|
||||||
const accountName = this._accountNameFromUserPath(url, 'inbox');
|
if (!activity.object || activity.object.type !== 'Follow') {
|
||||||
if (!accountName) {
|
return this.webServer.notImplemented(resp);
|
||||||
return this.webServer.resourceNotFound(resp);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
userFromAccount(accountName, (err, user) => {
|
Collection.removeFromCollectionById(
|
||||||
if (err) {
|
'followers',
|
||||||
return this.webServer.resourceNotFound(resp);
|
localUser,
|
||||||
}
|
remoteActor.id,
|
||||||
|
err => {
|
||||||
// we only understand Follow right now
|
if (err) {
|
||||||
if (!activity.object || activity.object.type !== 'Follow') {
|
return this.webServer.internalServerError(resp, err);
|
||||||
return this.webServer.notImplemented(resp);
|
|
||||||
}
|
|
||||||
|
|
||||||
Collection.removeFromCollectionById(
|
|
||||||
'followers',
|
|
||||||
user,
|
|
||||||
activity.actor,
|
|
||||||
err => {
|
|
||||||
if (err) {
|
|
||||||
return this.webServer.internalServerError(resp, err);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.log.info(
|
|
||||||
{ userId: user.userId, actor: activity.actor },
|
|
||||||
'Undo "Follow" (un-follow) success'
|
|
||||||
);
|
|
||||||
|
|
||||||
return this.webServer.accepted(resp);
|
|
||||||
}
|
}
|
||||||
);
|
|
||||||
});
|
this.log.info(
|
||||||
|
{
|
||||||
|
username: localUser.username,
|
||||||
|
userId: localUser.userId,
|
||||||
|
actor: remoteActor.id,
|
||||||
|
},
|
||||||
|
'Undo "Follow" (un-follow) success'
|
||||||
|
);
|
||||||
|
|
||||||
|
return this.webServer.accepted(resp);
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_withUserRequestHandler(signature, activity, activityHandler, req, resp) {
|
_collectionRequestHandler(
|
||||||
this.log.debug({ actor: activity.actor }, `Inbox request from ${activity.actor}`);
|
signature,
|
||||||
|
collectionName,
|
||||||
// :TODO: trace
|
activity,
|
||||||
const accountName = accountFromSelfUrl(activity.object);
|
activityHandler,
|
||||||
if (!accountName) {
|
req,
|
||||||
return this.webServer.badRequest(resp);
|
resp
|
||||||
|
) {
|
||||||
|
// turn a collection URL to a Actor ID
|
||||||
|
let actorId = this.webServer.fullUrl(req).toString();
|
||||||
|
const suffix = `/${collectionName}`;
|
||||||
|
if (actorId.endsWith(suffix)) {
|
||||||
|
actorId = actorId.slice(0, -suffix.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
userFromAccount(accountName, (err, user) => {
|
userFromActorId(actorId, (err, localUser) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
return this.webServer.resourceNotFound(resp);
|
return this.webServer.resourceNotFound(resp);
|
||||||
}
|
}
|
||||||
|
@ -604,7 +613,7 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
|
||||||
return this.webServer.accessDenied(resp);
|
return this.webServer.accessDenied(resp);
|
||||||
}
|
}
|
||||||
|
|
||||||
return activityHandler(activity, actor, user, resp);
|
return activityHandler(activity, actor, localUser, resp);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -613,7 +622,12 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
|
||||||
async.series(
|
async.series(
|
||||||
[
|
[
|
||||||
callback => {
|
callback => {
|
||||||
return Collection.addFollower(localUser, remoteActor, callback);
|
return Collection.addFollower(
|
||||||
|
localUser,
|
||||||
|
remoteActor,
|
||||||
|
this.webServer,
|
||||||
|
callback
|
||||||
|
);
|
||||||
},
|
},
|
||||||
callback => {
|
callback => {
|
||||||
Actor.fromLocalUser(localUser, this.webServer, (err, localActor) => {
|
Actor.fromLocalUser(localUser, this.webServer, (err, localActor) => {
|
||||||
|
@ -739,4 +753,50 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_prepareNewUserAsActor(user, cb) {
|
||||||
|
this.log.info(
|
||||||
|
{ username: user.username, userId: user.userId },
|
||||||
|
`Preparing ActivityPub settings for "${user.username}"`
|
||||||
|
);
|
||||||
|
|
||||||
|
const actorId = localActorId(this.webServer, user);
|
||||||
|
user.setProperty(UserProps.ActivityPubActorId, actorId);
|
||||||
|
|
||||||
|
user.updateActivityPubKeyPairProperties(err => {
|
||||||
|
if (err) {
|
||||||
|
return cb(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
user.generateNewRandomAvatar((err, outPath) => {
|
||||||
|
if (err) {
|
||||||
|
this.log.warn(
|
||||||
|
{
|
||||||
|
username: user.username,
|
||||||
|
userId: user.userId,
|
||||||
|
error: err.message,
|
||||||
|
},
|
||||||
|
`Failed to generate random avatar for "${user.username}"`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// :TODO: fetch over +op default overrides here, e.g. 'enabled'
|
||||||
|
const apSettings = ActivityPubSettings.fromUser(user);
|
||||||
|
|
||||||
|
const filename = paths.basename(outPath);
|
||||||
|
const avatarUrl =
|
||||||
|
makeUserUrl(this.webServer, user, '/users/') + `/avatar/${filename}`;
|
||||||
|
|
||||||
|
apSettings.image = avatarUrl;
|
||||||
|
apSettings.icon = avatarUrl;
|
||||||
|
|
||||||
|
user.setProperty(
|
||||||
|
UserProps.ActivityPubSettings,
|
||||||
|
JSON.stringify(apSettings)
|
||||||
|
);
|
||||||
|
|
||||||
|
return cb(null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -44,7 +44,7 @@ exports.getModule = class SystemGeneralWebHandler extends WebHandlerModule {
|
||||||
}
|
}
|
||||||
|
|
||||||
_avatarGetHandler(req, resp) {
|
_avatarGetHandler(req, resp) {
|
||||||
const url = new URL(req.url, `https://${req.headers.host}`);
|
const url = this.webServer.fullUrl(req);
|
||||||
const filename = paths.basename(url.pathname);
|
const filename = paths.basename(url.pathname);
|
||||||
if (!filename) {
|
if (!filename) {
|
||||||
return this.webServer.fileNotFound(resp);
|
return this.webServer.fileNotFound(resp);
|
||||||
|
|
|
@ -1,17 +1,21 @@
|
||||||
const WebHandlerModule = require('../../../web_handler_module');
|
const WebHandlerModule = require('../../../web_handler_module');
|
||||||
const Config = require('../../../config').get;
|
const Config = require('../../../config').get;
|
||||||
const { Errors } = require('../../../enig_error');
|
const { Errors, ErrorReasons } = require('../../../enig_error');
|
||||||
const { WellKnownLocations } = require('../web');
|
const { WellKnownLocations } = require('../web');
|
||||||
const {
|
const {
|
||||||
selfUrl,
|
localActorId,
|
||||||
webFingerProfileUrl,
|
webFingerProfileUrl,
|
||||||
userFromAccount,
|
userFromActorId,
|
||||||
getUserProfileTemplatedBody,
|
getUserProfileTemplatedBody,
|
||||||
DefaultProfileTemplate,
|
DefaultProfileTemplate,
|
||||||
} = require('../../../activitypub/util');
|
} = require('../../../activitypub/util');
|
||||||
|
|
||||||
const _ = require('lodash');
|
|
||||||
const EngiAssert = require('../../../enigma_assert');
|
const EngiAssert = require('../../../enigma_assert');
|
||||||
|
const User = require('../../../user');
|
||||||
|
const UserProps = require('../../../user_property');
|
||||||
|
const ActivityPubSettings = require('../../../activitypub/settings');
|
||||||
|
|
||||||
|
// deps
|
||||||
|
const _ = require('lodash');
|
||||||
|
|
||||||
exports.moduleInfo = {
|
exports.moduleInfo = {
|
||||||
name: 'WebFinger',
|
name: 'WebFinger',
|
||||||
|
@ -74,31 +78,12 @@ exports.getModule = class WebFingerWebHandler extends WebHandlerModule {
|
||||||
}
|
}
|
||||||
|
|
||||||
_profileRequestHandler(req, resp) {
|
_profileRequestHandler(req, resp) {
|
||||||
const url = new URL(req.url, `https://${req.headers.host}`);
|
const actorId = this.webServer.fullUrl(req).toString();
|
||||||
|
userFromActorId(actorId, (err, localUser) => {
|
||||||
const resource = url.pathname;
|
|
||||||
if (_.isEmpty(resource)) {
|
|
||||||
return this.webServer.instance.respondWithError(
|
|
||||||
resp,
|
|
||||||
400,
|
|
||||||
'pathname is required',
|
|
||||||
'Missing "resource"'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const userPosition = resource.indexOf('@');
|
|
||||||
if (-1 === userPosition || userPosition === resource.length - 1) {
|
|
||||||
this.webServer.resourceNotFound(resp);
|
|
||||||
return Errors.DoesNotExist('"@username" missing from path');
|
|
||||||
}
|
|
||||||
|
|
||||||
const accountName = resource.substring(userPosition + 1);
|
|
||||||
|
|
||||||
userFromAccount(accountName, (err, user) => {
|
|
||||||
if (err) {
|
if (err) {
|
||||||
this.log.warn(
|
this.log.warn(
|
||||||
{ url: req.url, error: err.message, type: 'Profile' },
|
{ error: err.message, type: 'Profile' },
|
||||||
`No profile for "${accountName}" could be retrieved`
|
'Could not fetch profile for WebFinger request'
|
||||||
);
|
);
|
||||||
return this.webServer.resourceNotFound(resp);
|
return this.webServer.resourceNotFound(resp);
|
||||||
}
|
}
|
||||||
|
@ -113,7 +98,7 @@ exports.getModule = class WebFingerWebHandler extends WebHandlerModule {
|
||||||
|
|
||||||
getUserProfileTemplatedBody(
|
getUserProfileTemplatedBody(
|
||||||
templateFile,
|
templateFile,
|
||||||
user,
|
localUser,
|
||||||
DefaultProfileTemplate,
|
DefaultProfileTemplate,
|
||||||
'text/plain',
|
'text/plain',
|
||||||
(err, body, contentType) => {
|
(err, body, contentType) => {
|
||||||
|
@ -134,8 +119,7 @@ exports.getModule = class WebFingerWebHandler extends WebHandlerModule {
|
||||||
}
|
}
|
||||||
|
|
||||||
_webFingerRequestHandler(req, resp) {
|
_webFingerRequestHandler(req, resp) {
|
||||||
const url = new URL(req.url, `https://${req.headers.host}`);
|
const url = this.webServer.fullUrl(req);
|
||||||
|
|
||||||
const resource = url.searchParams.get('resource');
|
const resource = url.searchParams.get('resource');
|
||||||
if (!resource) {
|
if (!resource) {
|
||||||
return this.webServer.respondWithError(
|
return this.webServer.respondWithError(
|
||||||
|
@ -148,13 +132,11 @@ exports.getModule = class WebFingerWebHandler extends WebHandlerModule {
|
||||||
|
|
||||||
const accountName = this._getAccountName(resource);
|
const accountName = this._getAccountName(resource);
|
||||||
if (!accountName || accountName.length < 1) {
|
if (!accountName || accountName.length < 1) {
|
||||||
this.webServer.resourceNotFound(resp);
|
this.log.warn(`Failed to parse "account name" for resource: ${resource}`);
|
||||||
return Errors.DoesNotExist(
|
return this.webServer.resourceNotFound(resp);
|
||||||
`Failed to parse "account name" for resource: ${resource}`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
userFromAccount(accountName, (err, user) => {
|
this._localUserFromWebFingerAccountName(accountName, (err, localUser) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
this.log.warn(
|
this.log.warn(
|
||||||
{ url: req.url, error: err.message, type: 'WebFinger' },
|
{ url: req.url, error: err.message, type: 'WebFinger' },
|
||||||
|
@ -166,11 +148,11 @@ exports.getModule = class WebFingerWebHandler extends WebHandlerModule {
|
||||||
const domain = this.webServer.getDomain();
|
const domain = this.webServer.getDomain();
|
||||||
|
|
||||||
const body = JSON.stringify({
|
const body = JSON.stringify({
|
||||||
subject: `acct:${user.username}@${domain}`,
|
subject: `acct:${localUser.username}@${domain}`,
|
||||||
aliases: [this._profileUrl(user), this._selfUrl(user)],
|
aliases: [this._profileUrl(localUser), this._userActorId(localUser)],
|
||||||
links: [
|
links: [
|
||||||
this._profilePageLink(user),
|
this._profilePageLink(localUser),
|
||||||
this._selfLink(user),
|
this._selfLink(localUser),
|
||||||
this._subscribeLink(),
|
this._subscribeLink(),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
@ -185,6 +167,41 @@ exports.getModule = class WebFingerWebHandler extends WebHandlerModule {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_localUserFromWebFingerAccountName(accountName, cb) {
|
||||||
|
if (accountName.startsWith('@')) {
|
||||||
|
accountName = accountName.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
User.getUserIdAndName(accountName, (err, userId) => {
|
||||||
|
if (err) {
|
||||||
|
return cb(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
User.getUser(userId, (err, user) => {
|
||||||
|
if (err) {
|
||||||
|
return cb(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
const accountStatus = user.getPropertyAsNumber(UserProps.AccountStatus);
|
||||||
|
if (
|
||||||
|
User.AccountStatus.disabled == accountStatus ||
|
||||||
|
User.AccountStatus.inactive == accountStatus
|
||||||
|
) {
|
||||||
|
return cb(
|
||||||
|
Errors.AccessDenied('Account disabled', ErrorReasons.Disabled)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const activityPubSettings = ActivityPubSettings.fromUser(user);
|
||||||
|
if (!activityPubSettings.enabled) {
|
||||||
|
return cb(Errors.AccessDenied('ActivityPub is not enabled for user'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return cb(null, user);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
_profileUrl(user) {
|
_profileUrl(user) {
|
||||||
return webFingerProfileUrl(this.webServer, user);
|
return webFingerProfileUrl(this.webServer, user);
|
||||||
}
|
}
|
||||||
|
@ -198,13 +215,13 @@ exports.getModule = class WebFingerWebHandler extends WebHandlerModule {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
_selfUrl(user) {
|
_userActorId(user) {
|
||||||
return selfUrl(this.webServer, user);
|
return localActorId(this.webServer, user);
|
||||||
}
|
}
|
||||||
|
|
||||||
// :TODO: only if ActivityPub is enabled
|
// :TODO: only if ActivityPub is enabled
|
||||||
_selfLink(user) {
|
_selfLink(user) {
|
||||||
const href = this._selfUrl(user);
|
const href = this._userActorId(user);
|
||||||
return {
|
return {
|
||||||
rel: 'self',
|
rel: 'self',
|
||||||
type: 'application/activity+json',
|
type: 'application/activity+json',
|
||||||
|
|
|
@ -10,7 +10,9 @@ module.exports = {
|
||||||
ConfigChanged: 'codes.l33t.enigma.system.config_changed', // (config.hjson)
|
ConfigChanged: 'codes.l33t.enigma.system.config_changed', // (config.hjson)
|
||||||
MenusChanged: 'codes.l33t.enigma.system.menus_changed', // (menu.hjson)
|
MenusChanged: 'codes.l33t.enigma.system.menus_changed', // (menu.hjson)
|
||||||
|
|
||||||
// User - includes { user, ...}
|
// User - includes { user, callback, ... } where user *is* the user instance in question
|
||||||
|
NewUserPrePersist: 'codes.l33t.enigma.system.user_new_pre_persist',
|
||||||
|
// User - includes { user, ...} where user is a *copy*
|
||||||
NewUser: 'codes.l33t.enigma.system.user_new', // { ... }
|
NewUser: 'codes.l33t.enigma.system.user_new', // { ... }
|
||||||
UserLogin: 'codes.l33t.enigma.system.user_login', // { ... }
|
UserLogin: 'codes.l33t.enigma.system.user_login', // { ... }
|
||||||
UserLogoff: 'codes.l33t.enigma.system.user_logoff', // { ... }
|
UserLogoff: 'codes.l33t.enigma.system.user_logoff', // { ... }
|
||||||
|
|
67
core/user.js
67
core/user.js
|
@ -22,7 +22,6 @@ const ssh2 = require('ssh2');
|
||||||
const AvatarGenerator = require('avatar-generator');
|
const AvatarGenerator = require('avatar-generator');
|
||||||
const paths = require('path');
|
const paths = require('path');
|
||||||
const fse = require('fs-extra');
|
const fse = require('fs-extra');
|
||||||
const ActivityPubSettings = require('./activitypub/settings');
|
|
||||||
|
|
||||||
module.exports = class User {
|
module.exports = class User {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
@ -49,6 +48,7 @@ module.exports = class User {
|
||||||
|
|
||||||
static get PBKDF2() {
|
static get PBKDF2() {
|
||||||
return {
|
return {
|
||||||
|
// :TODO: bump up iterations for all new PWs
|
||||||
iterations: 1000,
|
iterations: 1000,
|
||||||
keyLen: 128,
|
keyLen: 128,
|
||||||
saltLen: 32,
|
saltLen: 32,
|
||||||
|
@ -129,8 +129,14 @@ module.exports = class User {
|
||||||
}
|
}
|
||||||
|
|
||||||
getSanitizedName(type = 'username') {
|
getSanitizedName(type = 'username') {
|
||||||
const name =
|
let name;
|
||||||
'real' === type ? this.getProperty(UserProps.RealName) : this.username;
|
switch (type) {
|
||||||
|
case 'real':
|
||||||
|
name = this.getProperty(UserProps.RealName) || this.username;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
name = this.username;
|
||||||
|
}
|
||||||
return sanatizeFilename(name) || `user${this.userId.toString()}`;
|
return sanatizeFilename(name) || `user${this.userId.toString()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -507,46 +513,6 @@ module.exports = class User {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
function setKeyPair(trans, callback) {
|
|
||||||
self.updateActivityPubKeyPairProperties(err => {
|
|
||||||
return callback(err, trans);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
function defaultAvatar(trans, callback) {
|
|
||||||
self.generateNewRandomAvatar((err, outPath) => {
|
|
||||||
return callback(err, outPath, trans);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
function defaultActivityPubSettings(outPath, trans, callback) {
|
|
||||||
// we have to late import this crap :D
|
|
||||||
const getServer = require('./listening_server.js').getServer;
|
|
||||||
const WebServerPackageName = require('./servers/content/web')
|
|
||||||
.moduleInfo.packageName;
|
|
||||||
const webServer = getServer(WebServerPackageName);
|
|
||||||
|
|
||||||
// :TODO: fetch over +op default overrides here, e.g. 'enabled'
|
|
||||||
const apSettings = ActivityPubSettings.fromUser(self);
|
|
||||||
|
|
||||||
// convert |outPath| of avatar to a URL, that, with the web
|
|
||||||
// server enabled, can be fetched
|
|
||||||
if (webServer) {
|
|
||||||
const { makeUserUrl } = require('./activitypub/util');
|
|
||||||
const filename = paths.basename(outPath);
|
|
||||||
const url =
|
|
||||||
makeUserUrl(webServer.instance, self, '/users/') +
|
|
||||||
`/avatar/${filename}`;
|
|
||||||
|
|
||||||
apSettings.image = url;
|
|
||||||
apSettings.icon = url;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.setProperty(
|
|
||||||
UserProps.ActivityPubSettings,
|
|
||||||
JSON.stringify(apSettings)
|
|
||||||
);
|
|
||||||
|
|
||||||
return callback(null, trans);
|
|
||||||
},
|
|
||||||
function setInitialGroupMembership(trans, callback) {
|
function setInitialGroupMembership(trans, callback) {
|
||||||
// Assign initial groups. Must perform a clone: #235 - All users are sysops (and I can't un-sysop them)
|
// Assign initial groups. Must perform a clone: #235 - All users are sysops (and I can't un-sysop them)
|
||||||
self.groups = [...config.users.defaultGroups];
|
self.groups = [...config.users.defaultGroups];
|
||||||
|
@ -558,12 +524,21 @@ module.exports = class User {
|
||||||
|
|
||||||
return callback(null, trans);
|
return callback(null, trans);
|
||||||
},
|
},
|
||||||
|
function newUserPreEvent(trans, callback) {
|
||||||
|
Events.emit(Events.getSystemEvents().NewUserPrePersist, {
|
||||||
|
user: self,
|
||||||
|
sessionId: createUserInfo.sessionId,
|
||||||
|
callback: err => {
|
||||||
|
return callback(err, trans);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
function saveAll(trans, callback) {
|
function saveAll(trans, callback) {
|
||||||
self.persistWithTransaction(trans, err => {
|
self.persistWithTransaction(trans, err => {
|
||||||
return callback(err, trans);
|
return callback(err, trans);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
function sendEvent(trans, callback) {
|
function newUserEvent(trans, callback) {
|
||||||
Events.emit(Events.getSystemEvents().NewUser, {
|
Events.emit(Events.getSystemEvents().NewUser, {
|
||||||
user: Object.assign({}, self, {
|
user: Object.assign({}, self, {
|
||||||
sessionId: createUserInfo.sessionId,
|
sessionId: createUserInfo.sessionId,
|
||||||
|
@ -1048,8 +1023,8 @@ module.exports = class User {
|
||||||
userIds.push(row.user_id);
|
userIds.push(row.user_id);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
() => {
|
err => {
|
||||||
return cb(null, userIds);
|
return cb(err, userIds);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -70,5 +70,6 @@ module.exports = {
|
||||||
PublicActivityPubSigningKey: 'public_key_activitypub_sign_rsa_pem', // RSA public key for ActivityPub signing
|
PublicActivityPubSigningKey: 'public_key_activitypub_sign_rsa_pem', // RSA public key for ActivityPub signing
|
||||||
PrivateActivityPubSigningKey: 'private_key_activitypub_sign_rsa_pem', // RSA private key (corresponding to PublicActivityPubSigningKey)
|
PrivateActivityPubSigningKey: 'private_key_activitypub_sign_rsa_pem', // RSA private key (corresponding to PublicActivityPubSigningKey)
|
||||||
|
|
||||||
ActivityPubSettings: 'activity_pub_settings', // JSON object (above); see ActivityPubSettings in activitypub/settings.js
|
ActivityPubSettings: 'activitypub_settings', // JSON object (above); see ActivityPubSettings in activitypub/settings.js
|
||||||
|
ActivityPubActorId: 'activitypub_actor_id', // Actor ID representing this users
|
||||||
};
|
};
|
||||||
|
|
|
@ -65,7 +65,6 @@ exports.getModule = class WaitingForCallerModule extends MenuModule {
|
||||||
//
|
//
|
||||||
// Enforce that we have at least a secure connection in our ACS check
|
// Enforce that we have at least a secure connection in our ACS check
|
||||||
//
|
//
|
||||||
this.config.acs = this.config.acs;
|
|
||||||
if (!this.config.acs) {
|
if (!this.config.acs) {
|
||||||
this.config.acs = DefaultACS;
|
this.config.acs = DefaultACS;
|
||||||
} else if (!this.config.acs.includes('SC')) {
|
} else if (!this.config.acs.includes('SC')) {
|
||||||
|
|
Loading…
Reference in New Issue