WIP on shared inbox functionality
This commit is contained in:
parent
8f131630ff
commit
0fc8ae0e18
|
@ -23,6 +23,16 @@ module.exports = class Activity extends ActivityPubObject {
|
||||||
return WellKnownActivityTypes;
|
return WellKnownActivityTypes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static makeFollow(webServer, localActor, remoteActor, id = null) {
|
||||||
|
id = id || Activity._makeFullId(webServer, 'follow');
|
||||||
|
return new Activity({
|
||||||
|
id,
|
||||||
|
type: 'Follow',
|
||||||
|
actor: localActor,
|
||||||
|
object: remoteActor.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// https://www.w3.org/TR/activitypub/#accept-activity-inbox
|
// https://www.w3.org/TR/activitypub/#accept-activity-inbox
|
||||||
static makeAccept(webServer, localActor, followRequest, id = null) {
|
static makeAccept(webServer, localActor, followRequest, id = null) {
|
||||||
id = id || Activity._makeFullId(webServer, 'accept');
|
id = id || Activity._makeFullId(webServer, 'accept');
|
||||||
|
|
|
@ -9,6 +9,7 @@ const {
|
||||||
makeUserUrl,
|
makeUserUrl,
|
||||||
selfUrl,
|
selfUrl,
|
||||||
isValidLink,
|
isValidLink,
|
||||||
|
makeSharedInboxUrl,
|
||||||
} = require('../activitypub/util');
|
} = require('../activitypub/util');
|
||||||
const { ActivityStreamsContext } = require('./const');
|
const { ActivityStreamsContext } = require('./const');
|
||||||
const Log = require('../logger').log;
|
const Log = require('../logger').log;
|
||||||
|
@ -86,9 +87,11 @@ module.exports = class Actor extends ActivityPubObject {
|
||||||
id: userSelfUrl,
|
id: userSelfUrl,
|
||||||
type: 'Person',
|
type: 'Person',
|
||||||
preferredUsername: user.username,
|
preferredUsername: user.username,
|
||||||
name: user.getSanitizedName('real'),
|
name: userSettings.showRealName
|
||||||
|
? user.getSanitizedName('real')
|
||||||
|
: user.username,
|
||||||
endpoints: {
|
endpoints: {
|
||||||
sharedInbox: 'TODO',
|
sharedInbox: makeSharedInboxUrl(webServer),
|
||||||
},
|
},
|
||||||
inbox: makeUserUrl(webServer, user, '/ap/users/') + '/inbox',
|
inbox: makeUserUrl(webServer, user, '/ap/users/') + '/inbox',
|
||||||
outbox: makeUserUrl(webServer, user, '/ap/users/') + '/outbox',
|
outbox: makeUserUrl(webServer, user, '/ap/users/') + '/outbox',
|
||||||
|
|
|
@ -3,13 +3,20 @@ const ActivityPubObject = require('./object');
|
||||||
const apDb = require('../database').dbs.activitypub;
|
const apDb = require('../database').dbs.activitypub;
|
||||||
const { getISOTimestampString } = require('../database');
|
const { getISOTimestampString } = require('../database');
|
||||||
|
|
||||||
const { isString, get } = require('lodash');
|
const { isString, get, isObject } = require('lodash');
|
||||||
|
|
||||||
|
const APPublicCollectionId = 'https://www.w3.org/ns/activitystreams#Public';
|
||||||
|
const APPublicOwningUserId = 0;
|
||||||
|
|
||||||
module.exports = class Collection extends ActivityPubObject {
|
module.exports = class Collection extends ActivityPubObject {
|
||||||
constructor(obj) {
|
constructor(obj) {
|
||||||
super(obj);
|
super(obj);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static get PublicCollectionId() {
|
||||||
|
return APPublicCollectionId;
|
||||||
|
}
|
||||||
|
|
||||||
static followers(owningUser, page, webServer, cb) {
|
static followers(owningUser, page, webServer, cb) {
|
||||||
return Collection.getOrdered(
|
return Collection.getOrdered(
|
||||||
'followers',
|
'followers',
|
||||||
|
@ -68,17 +75,29 @@ module.exports = class Collection extends ActivityPubObject {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static addPublicInboxItem(inboxItem, cb) {
|
||||||
|
return Collection.addToCollection(
|
||||||
|
'publicInbox',
|
||||||
|
APPublicOwningUserId,
|
||||||
|
inboxItem.id,
|
||||||
|
inboxItem,
|
||||||
|
false,
|
||||||
|
cb
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
static getOrdered(name, owningUser, includePrivate, page, mapper, webServer, cb) {
|
static getOrdered(name, owningUser, includePrivate, page, mapper, webServer, cb) {
|
||||||
const privateQuery = includePrivate ? '' : ' AND is_private = FALSE';
|
const privateQuery = includePrivate ? '' : ' AND is_private = FALSE';
|
||||||
const followersUrl =
|
const followersUrl =
|
||||||
makeUserUrl(webServer, owningUser, '/ap/users/') + `/${name}`;
|
makeUserUrl(webServer, owningUser, '/ap/users/') + `/${name}`;
|
||||||
|
const owningUserId = isObject(owningUser) ? owningUser.userId : owningUser;
|
||||||
|
|
||||||
if (!page) {
|
if (!page) {
|
||||||
return apDb.get(
|
return apDb.get(
|
||||||
`SELECT COUNT(id) AS count
|
`SELECT COUNT(id) AS count
|
||||||
FROM collection
|
FROM collection
|
||||||
WHERE user_id = ? AND name = ?${privateQuery};`,
|
WHERE user_id = ? AND name = ?${privateQuery};`,
|
||||||
[owningUser.userId, name],
|
[owningUserId, name],
|
||||||
(err, row) => {
|
(err, row) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
|
@ -118,7 +137,7 @@ module.exports = class Collection extends ActivityPubObject {
|
||||||
FROM collection
|
FROM collection
|
||||||
WHERE user_id = ? AND name = ?${privateQuery}
|
WHERE user_id = ? AND name = ?${privateQuery}
|
||||||
ORDER BY timestamp;`,
|
ORDER BY timestamp;`,
|
||||||
[owningUser.userId, name],
|
[owningUserId, name],
|
||||||
(err, entries) => {
|
(err, entries) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
|
@ -147,11 +166,12 @@ module.exports = class Collection extends ActivityPubObject {
|
||||||
obj = JSON.stringify(obj);
|
obj = JSON.stringify(obj);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const owningUserId = isObject(owningUser) ? owningUser.userId : owningUser;
|
||||||
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, user_id, obj_id, obj_json, is_private)
|
||||||
VALUES (?, ?, ?, ?, ?, ?);`,
|
VALUES (?, ?, ?, ?, ?, ?);`,
|
||||||
[name, getISOTimestampString(), owningUser.userId, objectId, obj, isPrivate],
|
[name, getISOTimestampString(), owningUserId, objectId, obj, isPrivate],
|
||||||
function res(err) {
|
function res(err) {
|
||||||
// non-arrow for 'this' scope
|
// non-arrow for 'this' scope
|
||||||
if (err) {
|
if (err) {
|
||||||
|
@ -163,10 +183,11 @@ module.exports = class Collection extends ActivityPubObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
static removeFromCollectionById(name, owningUser, objectId, cb) {
|
static removeFromCollectionById(name, owningUser, objectId, cb) {
|
||||||
|
const owningUserId = isObject(owningUser) ? owningUser.userId : owningUser;
|
||||||
apDb.run(
|
apDb.run(
|
||||||
`DELETE FROM collection
|
`DELETE FROM collection
|
||||||
WHERE user_id = ? AND name = ? AND obj_id = ?;`,
|
WHERE user_id = ? AND name = ? AND obj_id = ?;`,
|
||||||
[owningUser.userId, name, objectId],
|
[owningUserId, name, objectId],
|
||||||
err => {
|
err => {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@ module.exports = class ActivityPubSettings {
|
||||||
this.enabled = true; // :TODO: fetch from +op config default
|
this.enabled = true; // :TODO: fetch from +op config default
|
||||||
this.manuallyApproveFollowers = false;
|
this.manuallyApproveFollowers = false;
|
||||||
this.hideSocialGraph = false; // followers, following
|
this.hideSocialGraph = false; // followers, following
|
||||||
this.showRealName = false;
|
this.showRealName = true;
|
||||||
this.image = '';
|
this.image = '';
|
||||||
this.icon = '';
|
this.icon = '';
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,7 @@ const moment = require('moment');
|
||||||
|
|
||||||
exports.ActivityStreamsContext = 'https://www.w3.org/ns/activitystreams';
|
exports.ActivityStreamsContext = 'https://www.w3.org/ns/activitystreams';
|
||||||
exports.isValidLink = isValidLink;
|
exports.isValidLink = isValidLink;
|
||||||
|
exports.makeSharedInboxUrl = makeSharedInboxUrl;
|
||||||
exports.makeUserUrl = makeUserUrl;
|
exports.makeUserUrl = makeUserUrl;
|
||||||
exports.webFingerProfileUrl = webFingerProfileUrl;
|
exports.webFingerProfileUrl = webFingerProfileUrl;
|
||||||
exports.selfUrl = selfUrl;
|
exports.selfUrl = selfUrl;
|
||||||
|
@ -39,6 +40,10 @@ function isValidLink(l) {
|
||||||
return /^https?:\/\/.+$/.test(l);
|
return /^https?:\/\/.+$/.test(l);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function makeSharedInboxUrl(webServer) {
|
||||||
|
return webServer.buildUrl(WellKnownLocations.Internal + '/ap/shared-inbox');
|
||||||
|
}
|
||||||
|
|
||||||
function makeUserUrl(webServer, user, relPrefix) {
|
function makeUserUrl(webServer, user, relPrefix) {
|
||||||
return webServer.buildUrl(
|
return webServer.buildUrl(
|
||||||
WellKnownLocations.Internal + `${relPrefix}${user.username}`
|
WellKnownLocations.Internal + `${relPrefix}${user.username}`
|
||||||
|
|
|
@ -529,7 +529,7 @@ dbs.message.run(
|
||||||
id INTEGER PRIMARY KEY, -- Auto-generated key
|
id INTEGER PRIMARY KEY, -- Auto-generated key
|
||||||
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
|
user_id INTEGER NOT NULL, -- Local, owning user ID, 0 means "all" for sharedInbox
|
||||||
obj_id VARCHAR NOT NULL, -- Object ID from obj_json.id
|
obj_id VARCHAR NOT NULL, -- Object ID from obj_json.id
|
||||||
obj_json VARCHAR NOT NULL, -- Object varies by collection (obj_json.type)
|
obj_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 |user_id|?
|
||||||
|
|
|
@ -56,6 +56,12 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.webServer.addRoute({
|
||||||
|
method: 'POST',
|
||||||
|
path: /^\/_enig\/ap\/shared-inbox$/,
|
||||||
|
handler: this._sharedInboxPostHandler.bind(this),
|
||||||
|
});
|
||||||
|
|
||||||
this.webServer.addRoute({
|
this.webServer.addRoute({
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
path: /^\/_enig\/ap\/users\/.+\/outbox(\?page=[0-9]+)?$/,
|
path: /^\/_enig\/ap\/users\/.+\/outbox(\?page=[0-9]+)?$/,
|
||||||
|
@ -162,20 +168,20 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
|
||||||
});
|
});
|
||||||
|
|
||||||
req.on('end', () => {
|
req.on('end', () => {
|
||||||
let activity;
|
const activity = Activity.fromJsonString(Buffer.concat(body).toString());
|
||||||
try {
|
if (!activity) {
|
||||||
activity = JSON.parse(Buffer.concat(body).toString());
|
|
||||||
activity = new Activity(activity);
|
|
||||||
} catch (e) {
|
|
||||||
this.log.error(
|
this.log.error(
|
||||||
{ error: e.message, url: req.url, method: req.method },
|
{ url: req.url, method: req.method, endpoint: 'inbox' },
|
||||||
'Failed to parse Activity'
|
'Failed to parse Activity'
|
||||||
);
|
);
|
||||||
return this.webServer.resourceNotFound(resp);
|
return this.webServer.resourceNotFound(resp);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!activity.isValid()) {
|
if (!activity.isValid()) {
|
||||||
this.log.warn({ activity }, 'Invalid or unsupported Activity');
|
this.log.warn(
|
||||||
|
{ activity, endpoint: 'inbox' },
|
||||||
|
'Invalid or unsupported Activity'
|
||||||
|
);
|
||||||
return this.webServer.badRequest(resp);
|
return this.webServer.badRequest(resp);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -192,6 +198,8 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
|
||||||
case 'Undo':
|
case 'Undo':
|
||||||
return this._inboxUndoRequestHandler(activity, req, resp);
|
return this._inboxUndoRequestHandler(activity, req, resp);
|
||||||
|
|
||||||
|
// :TODO: Create, etc.
|
||||||
|
|
||||||
default:
|
default:
|
||||||
this.log.warn(
|
this.log.warn(
|
||||||
{ type: activity.type },
|
{ type: activity.type },
|
||||||
|
@ -204,6 +212,100 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_sharedInboxPostHandler(req, resp) {
|
||||||
|
const body = [];
|
||||||
|
req.on('data', d => {
|
||||||
|
body.push(d);
|
||||||
|
});
|
||||||
|
|
||||||
|
req.on('end', () => {
|
||||||
|
const activity = Activity.fromJsonString(Buffer.concat(body).toString());
|
||||||
|
if (!activity) {
|
||||||
|
this.log.error(
|
||||||
|
{ url: req.url, method: req.method, endpoint: 'sharedInbox' },
|
||||||
|
'Failed to parse Activity'
|
||||||
|
);
|
||||||
|
return this.webServer.resourceNotFound(resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!activity.isValid()) {
|
||||||
|
this.log.warn(
|
||||||
|
{ activity, endpoint: 'sharedInbox' },
|
||||||
|
'Invalid or unsupported Activity'
|
||||||
|
);
|
||||||
|
return this.webServer.badRequest(resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (activity.type) {
|
||||||
|
case 'Create':
|
||||||
|
return this._sharedInboxCreateActivity(req, resp, activity);
|
||||||
|
|
||||||
|
default:
|
||||||
|
this.log.warn(
|
||||||
|
{ type: activity.type },
|
||||||
|
'Invalid or unknown Activity type'
|
||||||
|
);
|
||||||
|
return this.resourceNotFound(resp);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_sharedInboxCreateActivity(req, resp, activity) {
|
||||||
|
// When an object is being delivered to the originating actor's followers,
|
||||||
|
// a server MAY reduce the number of receiving actors delivered to by
|
||||||
|
// identifying all followers which share the same sharedInbox who would
|
||||||
|
// otherwise be individual recipients and instead deliver objects to said
|
||||||
|
// sharedInbox. Thus in this scenario, the remote/receiving server participates
|
||||||
|
// in determining targeting and performing delivery to specific inboxes.
|
||||||
|
let toActors = activity.to;
|
||||||
|
if (!Array.isArray(toActors)) {
|
||||||
|
toActors = [toActors];
|
||||||
|
}
|
||||||
|
|
||||||
|
const createWhat = _.get(activity, 'object.type');
|
||||||
|
switch (createWhat) {
|
||||||
|
case 'Note':
|
||||||
|
return this._deliverSharedInboxNote(req, resp, toActors, activity);
|
||||||
|
|
||||||
|
default:
|
||||||
|
this.log.warn(
|
||||||
|
{ type: createWhat },
|
||||||
|
'Invalid or unsupported "Create" type'
|
||||||
|
);
|
||||||
|
return this.resourceNotFound(resp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_deliverSharedInboxNote(req, resp, toActors, activity) {
|
||||||
|
async.forEach(
|
||||||
|
toActors,
|
||||||
|
(actor, nextActor) => {
|
||||||
|
if (Collection.PublicCollectionId === actor) {
|
||||||
|
// Deliver to inbox for "everyone":
|
||||||
|
// - Add to 'sharedInbox' collection
|
||||||
|
//
|
||||||
|
Collection.addPublicInboxItem(activity.object, err => {
|
||||||
|
if (err) {
|
||||||
|
return nextActor(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
return nextActor(null);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
nextActor(null);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
err => {
|
||||||
|
if (err) {
|
||||||
|
return this.webServer.internalServerError(resp, err);
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.writeHead(202);
|
||||||
|
return resp.end('');
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
_getCollectionHandler(name, req, resp, signature) {
|
_getCollectionHandler(name, req, resp, signature) {
|
||||||
EnigAssert(signature, 'Missing signature!');
|
EnigAssert(signature, 'Missing signature!');
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue