Start Collection, some extra Actor props, start Followers, cleanup/DRY/etc.
This commit is contained in:
parent
84dde6c5c5
commit
315d77b1c0
|
@ -151,6 +151,7 @@ module.exports = class Activity {
|
|||
);
|
||||
}
|
||||
|
||||
// :TODO: move to Collection
|
||||
static fromOutboxEntries(owningUser, webServer, cb) {
|
||||
// :TODO: support paging
|
||||
const getOpts = {
|
||||
|
@ -183,11 +184,11 @@ module.exports = class Activity {
|
|||
}
|
||||
|
||||
sendTo(actorUrl, fromUser, webServer, cb) {
|
||||
const privateKey = fromUser.getProperty(UserProps.PrivateKeyMain);
|
||||
const privateKey = fromUser.getProperty(UserProps.PrivateActivityPubSigningKey);
|
||||
if (_.isEmpty(privateKey)) {
|
||||
return cb(
|
||||
Errors.MissingProperty(
|
||||
`User "${fromUser.username}" is missing the '${UserProps.PrivateKeyMain}' property`
|
||||
`User "${fromUser.username}" is missing the '${UserProps.PrivateActivityPubSigningKey}' property`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -14,12 +14,13 @@ const {
|
|||
const Log = require('../logger').log;
|
||||
const { queryWebFinger } = require('../webfinger');
|
||||
const EnigAssert = require('../enigma_assert');
|
||||
const ActivityPubSettings = require('./settings');
|
||||
|
||||
// deps
|
||||
const _ = require('lodash');
|
||||
const https = require('https');
|
||||
|
||||
const isString = require('lodash/isString');
|
||||
const mimeTypes = require('mime-types');
|
||||
|
||||
// https://www.w3.org/TR/activitypub/#actor-objects
|
||||
module.exports = class Actor {
|
||||
|
@ -63,6 +64,21 @@ module.exports = class Actor {
|
|||
// :TODO: from a User object
|
||||
static fromLocalUser(user, webServer, cb) {
|
||||
const userSelfUrl = selfUrl(webServer, user);
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const obj = {
|
||||
'@context': [
|
||||
|
@ -82,7 +98,8 @@ module.exports = class Actor {
|
|||
following: makeUserUrl(webServer, user, '/ap/users/') + '/following',
|
||||
summary: user.getProperty(UserProps.AutoSignature) || '',
|
||||
url: webFingerProfileUrl(webServer, user),
|
||||
|
||||
manuallyApprovesFollowers: userSettings.manuallyApprovesFollowers,
|
||||
discoverable: userSettings.discoverable,
|
||||
// :TODO: we can start to define BBS related stuff with the community perhaps
|
||||
// attachment: [
|
||||
// {
|
||||
|
@ -93,6 +110,9 @@ module.exports = class Actor {
|
|||
// ],
|
||||
};
|
||||
|
||||
addImage('icon');
|
||||
addImage('image');
|
||||
|
||||
const publicKeyPem = user.getProperty(UserProps.PublicActivityPubSigningKey);
|
||||
if (!_.isEmpty(publicKeyPem)) {
|
||||
obj.publicKey = {
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
const { ActivityStreamsContext, makeUserUrl } = require('./util');
|
||||
const { FollowerEntryStatus, getFollowerEntries } = require('./db');
|
||||
|
||||
module.exports = class Collection {
|
||||
constructor(obj) {
|
||||
this['@context'] = ActivityStreamsContext;
|
||||
Object.assign(this, obj);
|
||||
}
|
||||
|
||||
static followers(owningUser, page, webServer, cb) {
|
||||
if (!page) {
|
||||
const followersUrl =
|
||||
makeUserUrl(webServer, owningUser, '/ap/users/') + '/followers';
|
||||
|
||||
const obj = {
|
||||
id: followersUrl,
|
||||
type: 'OrderedCollection',
|
||||
first: `${followersUrl}?page=1`,
|
||||
totalItems: 1,
|
||||
};
|
||||
|
||||
return cb(null, new Collection(obj));
|
||||
}
|
||||
|
||||
// :TODO: actually support paging...
|
||||
page = parseInt(page);
|
||||
const getOpts = {
|
||||
status: FollowerEntryStatus.Accepted,
|
||||
};
|
||||
getFollowerEntries(owningUser, getOpts, (err, followers) => {
|
||||
if (err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
const baseId = makeUserUrl(webServer, owningUser, '/ap/users') + '/followers';
|
||||
|
||||
const obj = {
|
||||
id: `${baseId}/page=${page}`,
|
||||
type: 'OrderedCollectionPage',
|
||||
totalItems: followers.length,
|
||||
orderedItems: followers,
|
||||
partOf: baseId,
|
||||
};
|
||||
|
||||
return cb(null, new Collection(obj));
|
||||
});
|
||||
}
|
||||
};
|
|
@ -2,6 +2,16 @@ const apDb = require('../database').dbs.activitypub;
|
|||
|
||||
exports.persistToOutbox = persistToOutbox;
|
||||
exports.getOutboxEntries = getOutboxEntries;
|
||||
exports.persistFollower = persistFollower;
|
||||
exports.getFollowerEntries = getFollowerEntries;
|
||||
|
||||
const FollowerEntryStatus = {
|
||||
Invalid: 0, // Invalid
|
||||
Requested: 1, // Entry is a *request* to local user
|
||||
Accepted: 2, // Accepted by local user
|
||||
Rejected: 3, // Rejected by local user
|
||||
};
|
||||
exports.FollowerEntryStatus = FollowerEntryStatus;
|
||||
|
||||
function persistToOutbox(activity, fromUser, message, cb) {
|
||||
const activityJson = JSON.stringify(activity);
|
||||
|
@ -55,3 +65,36 @@ function getOutboxEntries(owningUser, options, cb) {
|
|||
}
|
||||
);
|
||||
}
|
||||
|
||||
function persistFollower(localUser, remoteActor, options, cb) {
|
||||
const status = options.status || FollowerEntryStatus.Requested;
|
||||
|
||||
apDb.run(
|
||||
`INSERT OR IGNORE INTO followers (user_id, follower_id, status)
|
||||
VALUES (?, ?, ?);`,
|
||||
[localUser.userId, remoteActor.id, status],
|
||||
function res(err) {
|
||||
// non-arrow for 'this' scope
|
||||
return cb(err, this.lastID);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function getFollowerEntries(localUser, options, cb) {
|
||||
const status = options.status || FollowerEntryStatus.Accepted;
|
||||
|
||||
apDb.all(
|
||||
`SELECT follower_id
|
||||
FROM followers
|
||||
WHERE user_id = ? AND status = ?;`,
|
||||
[localUser.userId, status],
|
||||
(err, rows) => {
|
||||
if (err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
const entries = rows.map(r => r.follower_id);
|
||||
return cb(null, entries);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
@ -6,8 +6,8 @@ module.exports = class ActivityPubSettings {
|
|||
this.manuallyApproveFollowers = false;
|
||||
this.hideSocialGraph = false; // followers, following
|
||||
this.showRealName = false;
|
||||
this.imageUrl = '';
|
||||
this.iconUrl = '';
|
||||
this.image = '';
|
||||
this.icon = '';
|
||||
|
||||
if (obj) {
|
||||
Object.assign(this, obj);
|
||||
|
|
|
@ -531,6 +531,17 @@ dbs.message.run(
|
|||
ON outbox (json_extract(activity_json, '$.type'));`
|
||||
);
|
||||
|
||||
dbs.activitypub.run(
|
||||
`CREATE TABLE IF NOT EXISTS followers (
|
||||
id INTEGER PRIMARY KEY, -- Local ID
|
||||
user_id INTEGER NOT NULL, -- Local user ID
|
||||
follower_id VARCHAR NOT NULL, -- Actor ID of follower
|
||||
status INTEGER NOT NULL, -- Status: See FollowerEntryStatus
|
||||
|
||||
UNIQUE(user_id, follower_id)
|
||||
);`
|
||||
);
|
||||
|
||||
return cb(null);
|
||||
},
|
||||
};
|
||||
|
|
|
@ -9,11 +9,14 @@ const Config = require('../../../config').get;
|
|||
const Activity = require('../../../activitypub/activity');
|
||||
const ActivityPubSettings = require('../../../activitypub/settings');
|
||||
const Actor = require('../../../activitypub/actor');
|
||||
const Collection = require('../../../activitypub/collection');
|
||||
const { persistFollower, FollowerEntryStatus } = require('../../../activitypub/db');
|
||||
|
||||
// deps
|
||||
const _ = require('lodash');
|
||||
const enigma_assert = require('../../../enigma_assert');
|
||||
const httpSignature = require('http-signature');
|
||||
const async = require('async');
|
||||
|
||||
exports.moduleInfo = {
|
||||
name: 'ActivityPub',
|
||||
|
@ -49,10 +52,16 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
|
|||
|
||||
this.webServer.addRoute({
|
||||
method: 'GET',
|
||||
path: /^\/_enig\/ap\/users\/.+\/outbox(\?page=true)?$/,
|
||||
path: /^\/_enig\/ap\/users\/.+\/outbox$/,
|
||||
handler: this._outboxGetHandler.bind(this),
|
||||
});
|
||||
|
||||
this.webServer.addRoute({
|
||||
method: 'GET',
|
||||
path: /^\/_enig\/ap\/users\/.+\/followers(\?page=[0-9]+)?$/,
|
||||
handler: this._followersGetHandler.bind(this),
|
||||
});
|
||||
|
||||
// :TODO: NYI
|
||||
// this.webServer.addRoute({
|
||||
// method: 'GET',
|
||||
|
@ -164,12 +173,11 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
|
|||
|
||||
// /_enig/ap/users/SomeName/outbox -> SomeName
|
||||
const url = new URL(req.url, `https://${req.headers.host}`);
|
||||
const m = url.pathname.match(/^\/_enig\/ap\/users\/(.+)\/outbox$/);
|
||||
if (!m || !m[1]) {
|
||||
const accountName = this._accountNameFromUserPath(url, 'outbox');
|
||||
if (!accountName) {
|
||||
return this.webServer.resourceNotFound(resp);
|
||||
}
|
||||
|
||||
const accountName = m[1];
|
||||
userFromAccount(accountName, (err, user) => {
|
||||
if (err) {
|
||||
this.log.info(
|
||||
|
@ -197,6 +205,61 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
|
|||
});
|
||||
}
|
||||
|
||||
_accountNameFromUserPath(url, suffix) {
|
||||
const re = new RegExp(`^/_enig/ap/users/(.+)/${suffix}(\\?page=[0-9]+)?$`);
|
||||
const m = url.pathname.match(re);
|
||||
if (!m || !m[1]) {
|
||||
return null;
|
||||
}
|
||||
return m[1];
|
||||
}
|
||||
|
||||
_followersGetHandler(req, resp) {
|
||||
this.log.trace({ url: req.url }, 'Request for "followers"');
|
||||
|
||||
// :TODO: dry this stuff..
|
||||
|
||||
// the request must be signed, and the signature must be valid
|
||||
const signature = this._parseAndValidateSignature(req);
|
||||
if (!signature) {
|
||||
return this.webServer.accessDenied(resp);
|
||||
}
|
||||
|
||||
// /_enig/ap/users/SomeName/outbox -> SomeName
|
||||
const url = new URL(req.url, `https://${req.headers.host}`);
|
||||
const accountName = this._accountNameFromUserPath(url, 'followers');
|
||||
if (!accountName) {
|
||||
return this.webServer.resourceNotFound(resp);
|
||||
}
|
||||
|
||||
userFromAccount(accountName, (err, user) => {
|
||||
if (err) {
|
||||
this.log.info(
|
||||
{ reason: err.message, accountName: accountName },
|
||||
`No user "${accountName}" for "self"`
|
||||
);
|
||||
return this.webServer.resourceNotFound(resp);
|
||||
}
|
||||
|
||||
const page = url.searchParams.get('page');
|
||||
Collection.followers(user, page, this.webServe, (err, collection) => {
|
||||
if (err) {
|
||||
// :TODO: LOG ME
|
||||
return this.webServer.internalServerError(resp);
|
||||
}
|
||||
|
||||
const body = JSON.stringify(collection);
|
||||
const headers = {
|
||||
'Content-Type': ActivityJsonMime,
|
||||
'Content-Length': body.length,
|
||||
};
|
||||
|
||||
resp.writeHead(200, headers);
|
||||
return resp.end(body);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_parseAndValidateSignature(req) {
|
||||
let signature;
|
||||
try {
|
||||
|
@ -285,53 +348,7 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
|
|||
// :TODO: Implement the queue
|
||||
const activityPubSettings = ActivityPubSettings.fromUser(user);
|
||||
if (!activityPubSettings.manuallyApproveFollowers) {
|
||||
Actor.fromLocalUser(user, this.webServer, (err, localActor) => {
|
||||
if (err) {
|
||||
return this.log.warn(
|
||||
{ inbox: actor.inbox, error: err.message },
|
||||
'Failed to load local Actor for "Accept"'
|
||||
);
|
||||
}
|
||||
|
||||
const accept = Activity.makeAccept(
|
||||
this.webServer,
|
||||
localActor,
|
||||
activity
|
||||
);
|
||||
|
||||
accept.sendTo(
|
||||
actor.inbox,
|
||||
user,
|
||||
this.webServer,
|
||||
(err, respBody, res) => {
|
||||
if (err) {
|
||||
return this.log.warn(
|
||||
{
|
||||
inbox: actor.inbox,
|
||||
statusCode: res.statusCode,
|
||||
error: err.message,
|
||||
},
|
||||
'Failed POSTing "Accept" to inbox'
|
||||
);
|
||||
}
|
||||
|
||||
if (res.statusCode !== 202 && res.statusCode !== 200) {
|
||||
return this.log.warn(
|
||||
{
|
||||
inbox: actor.inbox,
|
||||
statusCode: res.statusCode,
|
||||
},
|
||||
'Unexpected status code'
|
||||
);
|
||||
}
|
||||
|
||||
this.log.trace(
|
||||
{ inbox: actor.inbox },
|
||||
'Remote server received our "Accept" successfully'
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
this._recordAcceptedFollowRequest(user, actor, activity);
|
||||
}
|
||||
|
||||
resp.writeHead(200, { 'Content-Type': 'text/html' });
|
||||
|
@ -340,6 +357,80 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
|
|||
});
|
||||
}
|
||||
|
||||
_recordAcceptedFollowRequest(localUser, remoteActor, requestActivity) {
|
||||
async.series(
|
||||
[
|
||||
callback => {
|
||||
const persistOpts = {
|
||||
status: FollowerEntryStatus.Accepted,
|
||||
};
|
||||
return persistFollower(localUser, remoteActor, persistOpts, callback);
|
||||
},
|
||||
callback => {
|
||||
Actor.fromLocalUser(localUser, this.webServer, (err, localActor) => {
|
||||
if (err) {
|
||||
this.log.warn(
|
||||
{ inbox: remoteActor.inbox, error: err.message },
|
||||
'Failed to load local Actor for "Accept"'
|
||||
);
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
const accept = Activity.makeAccept(
|
||||
this.webServer,
|
||||
localActor,
|
||||
requestActivity
|
||||
);
|
||||
|
||||
accept.sendTo(
|
||||
remoteActor.inbox,
|
||||
localUser,
|
||||
this.webServer,
|
||||
(err, respBody, res) => {
|
||||
if (err) {
|
||||
this.log.warn(
|
||||
{
|
||||
inbox: remoteActor.inbox,
|
||||
error: err.message,
|
||||
},
|
||||
'Failed POSTing "Accept" to inbox'
|
||||
);
|
||||
return callback(null); // just a warning
|
||||
}
|
||||
|
||||
if (res.statusCode !== 202 && res.statusCode !== 200) {
|
||||
this.log.warn(
|
||||
{
|
||||
inbox: remoteActor.inbox,
|
||||
statusCode: res.statusCode,
|
||||
},
|
||||
'Unexpected status code'
|
||||
);
|
||||
return callback(null); // just a warning
|
||||
}
|
||||
|
||||
this.log.trace(
|
||||
{ inbox: remoteActor.inbox },
|
||||
'Remote server received our "Accept" successfully'
|
||||
);
|
||||
|
||||
return callback(null);
|
||||
}
|
||||
);
|
||||
});
|
||||
},
|
||||
],
|
||||
err => {
|
||||
if (err) {
|
||||
this.log.error(
|
||||
{ error: err.message },
|
||||
'Failed processing Follow request'
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
_authorizeInteractionHandler(req, resp) {
|
||||
console.log(req);
|
||||
console.log(resp);
|
||||
|
|
Loading…
Reference in New Issue