Cleanup, DRY, logging

This commit is contained in:
Bryan Ashby 2023-01-20 22:15:59 -07:00
parent 9517b292a4
commit d9e4b66a35
No known key found for this signature in database
GPG Key ID: C2C1B501E4EFD994
5 changed files with 145 additions and 113 deletions

View File

@ -18,9 +18,9 @@ const ActivityPubSettings = require('./settings');
// deps // deps
const _ = require('lodash'); const _ = require('lodash');
const https = require('https');
const isString = require('lodash/isString'); const isString = require('lodash/isString');
const mimeTypes = require('mime-types'); const mimeTypes = require('mime-types');
const { getJson } = require('../http_util.js');
// https://www.w3.org/TR/activitypub/#actor-objects // https://www.w3.org/TR/activitypub/#actor-objects
module.exports = class Actor { module.exports = class Actor {
@ -141,40 +141,18 @@ module.exports = class Actor {
}; };
// :TODO: use getJson() // :TODO: use getJson()
getJson(url, { headers }, (err, actor) => {
https.get(url, { headers }, res => { if (err) {
if (res.statusCode !== 200) { return cb(err);
return cb(Errors.Invalid(`Bad HTTP status code: ${res.statusCode}`));
} }
const contentType = res.headers['content-type']; actor = new Actor(actor);
if (
!_.isString(contentType) || if (!actor.isValid()) {
!contentType.startsWith('application/activity+json') return cb(Errors.Invalid('Invalid Actor'));
) {
return cb(Errors.Invalid(`Invalid Content-Type: ${contentType}`));
} }
res.setEncoding('utf8'); return cb(null, actor);
let body = '';
res.on('data', data => {
body += data;
});
res.on('end', () => {
let actor;
try {
actor = Actor.fromJsonString(body);
} catch (e) {
return cb(e);
}
if (!actor.isValid()) {
return cb(Errors.Invalid('Invalid Actor'));
}
return cb(null, actor);
});
}); });
} }

View File

@ -2,13 +2,42 @@ const { makeUserUrl } = require('./util');
const ActivityPubObject = require('./object'); 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 } = require('lodash');
const { isString, get } = require('lodash');
module.exports = class Collection extends ActivityPubObject { module.exports = class Collection extends ActivityPubObject {
constructor(obj) { constructor(obj) {
super(obj); super(obj);
} }
static followers(owningUser, page, webServer, cb) {
return Collection.getOrdered(
'followers',
owningUser,
false,
page,
e => e.id,
webServer,
cb
);
}
static following(owningUser, page, webServer, cb) {
return Collection.getOrdered(
'following',
owningUser,
false,
page,
e => get(e, 'object.id'),
webServer,
cb
);
}
static addFollower(owningUser, followingActor, cb) {
return Collection.addToCollection('followers', owningUser, followingActor, cb);
}
static getOrdered(name, owningUser, includePrivate, page, mapper, webServer, cb) { static getOrdered(name, owningUser, includePrivate, page, mapper, webServer, cb) {
// :TODD: |includePrivate| handling // :TODD: |includePrivate| handling
const followersUrl = const followersUrl =
@ -24,12 +53,22 @@ module.exports = class Collection extends ActivityPubObject {
return cb(err); return cb(err);
} }
const obj = { let obj;
id: followersUrl, if (row.count > 0) {
type: 'OrderedCollection', obj = {
first: `${followersUrl}?page=1`, id: followersUrl,
totalItems: row.count, type: 'OrderedCollection',
}; first: `${followersUrl}?page=1`,
totalItems: row.count,
};
} else {
obj = {
id: followersUrl,
type: 'OrderedCollection',
totalItems: 0,
orderedItems: [],
};
}
return cb(null, new Collection(obj)); return cb(null, new Collection(obj));
} }
@ -48,7 +87,8 @@ module.exports = class Collection extends ActivityPubObject {
return cb(err); return cb(err);
} }
if (mapper) { entries = entries || [];
if (mapper && entries.length > 0) {
entries = entries.map(mapper); entries = entries.map(mapper);
} }
@ -65,35 +105,22 @@ module.exports = class Collection extends ActivityPubObject {
); );
} }
static followers(owningUser, page, webServer, cb) {
return Collection.getOrdered(
'followers',
owningUser,
false,
page,
e => e.id,
webServer,
cb
);
}
static addToCollection(name, owningUser, entry, cb) { static addToCollection(name, owningUser, entry, cb) {
if (!isString(entry)) { if (!isString(entry)) {
entry = JSON.stringify(entry); entry = JSON.stringify(entry);
} }
apDb.run( apDb.run(
`INSERT INTO collection_entry (name, timestamp, user_id, entry_json) `INSERT OR IGNORE INTO collection_entry (name, timestamp, user_id, entry_json)
VALUES (?, ?, ?, ?);`, VALUES (?, ?, ?, ?);`,
[name, getISOTimestampString(), owningUser.userId, entry], [name, getISOTimestampString(), owningUser.userId, entry],
function res(err) { function res(err) {
// non-arrow for 'this' scope // non-arrow for 'this' scope
if (err) {
return cb(err);
}
return cb(err, this.lastID); return cb(err, this.lastID);
} }
); );
} }
static addFollower(owningUser, followingActor, cb) {
return Collection.addToCollection('followers', owningUser, followingActor, cb);
}
}; };

View File

@ -542,8 +542,13 @@ dbs.message.run(
); );
dbs.activitypub.run( dbs.activitypub.run(
`CREATE INDEX IF NOT EXISTS collection_entry_unique_index0 `CREATE INDEX IF NOT EXISTS collection_entry_by_user_index0
ON collection_entry (name, user_id, json_extract(entry_json, '$.id'))` ON collection_entry (name, user_id);`
);
dbs.activitypub.run(
`CREATE UNIQUE INDEX IF NOT EXISTS collection_entry_unique_index0
ON collection_entry (name, user_id, json_extract(entry_json, '$.id'));`
); );
return cb(null); return cb(null);

View File

@ -334,7 +334,10 @@ exports.getModule = class WebServerModule extends ServerModule {
); );
} }
internalServerError(resp) { internalServerError(resp, err) {
if (err) {
this.log.error({ error: err.message }, 'Internal server error');
}
return this.respondWithError( return this.respondWithError(
resp, resp,
500, 500,

View File

@ -64,7 +64,25 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
this.webServer.addRoute({ this.webServer.addRoute({
method: 'GET', method: 'GET',
path: /^\/_enig\/ap\/users\/.+\/followers(\?page=[0-9]+)?$/, path: /^\/_enig\/ap\/users\/.+\/followers(\?page=[0-9]+)?$/,
handler: this._followersGetHandler.bind(this), handler: (req, resp) => {
return this._enforceSigningPolicy(
req,
resp,
this._followersGetHandler.bind(this)
);
},
});
this.webServer.addRoute({
method: 'GET',
path: /^\/_enig\/ap\/users\/.+\/following(\?page=[0-9]+)?$/,
handler: (req, resp) => {
return this._enforceSigningPolicy(
req,
resp,
this._followingGetHandler.bind(this)
);
},
}); });
// :TODO: NYI // :TODO: NYI
@ -180,9 +198,54 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
}); });
} }
_getCollectionHandler(name, req, resp) {
const url = new URL(req.url, `https://${req.headers.host}`);
const accountName = this._accountNameFromUserPath(url, name);
if (!accountName) {
return this.webServer.resourceNotFound(resp);
}
// can we even handle this request?
const getter = Collection[name];
if (!getter) {
return this.webServer.resourceNotFound(resp);
}
userFromAccount(accountName, (err, user) => {
if (err) {
this.log.info(
{ reason: err.message, accountName: accountName },
`No user "${accountName}" for "${name}"`
);
return this.webServer.resourceNotFound(resp);
}
const page = url.searchParams.get('page');
getter(user, page, this.webServer, (err, collection) => {
if (err) {
return this.webServer.internalServerError(resp, err);
}
const body = JSON.stringify(collection);
const headers = {
'Content-Type': ActivityJsonMime,
'Content-Length': body.length,
};
resp.writeHead(200, headers);
return resp.end(body);
});
});
}
_followingGetHandler(req, resp) {
this.log.debug({ url: req.url }, 'Request for "following"');
return this._getCollectionHandler('following', req, resp);
}
// https://docs.gotosocial.org/en/latest/federation/behaviors/outbox/ // https://docs.gotosocial.org/en/latest/federation/behaviors/outbox/
_outboxGetHandler(req, resp) { _outboxGetHandler(req, resp) {
this.log.trace({ url: req.url }, 'Request for "outbox"'); this.log.debug({ url: req.url }, 'Request for "outbox"');
// the request must be signed, and the signature must be valid // the request must be signed, and the signature must be valid
const signature = this._parseAndValidateSignature(req); const signature = this._parseAndValidateSignature(req);
@ -208,8 +271,7 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
Activity.fromOutboxEntries(user, this.webServer, (err, activity) => { Activity.fromOutboxEntries(user, this.webServer, (err, activity) => {
if (err) { if (err) {
// :TODO: LOG ME return this.webServer.internalServerError(resp, err);
return this.webServer.internalServerError(resp);
} }
const body = JSON.stringify(activity); const body = JSON.stringify(activity);
@ -234,49 +296,8 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
} }
_followersGetHandler(req, resp) { _followersGetHandler(req, resp) {
this.log.trace({ url: req.url }, 'Request for "followers"'); this.log.debug({ url: req.url }, 'Request for "followers"');
return this._getCollectionHandler('followers', req, resp);
// :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.webServer, (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) { _parseAndValidateSignature(req) {
@ -331,7 +352,7 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
} }
_withUserRequestHandler(signature, activity, activityHandler, req, resp) { _withUserRequestHandler(signature, activity, activityHandler, req, resp) {
this.log.trace({ actor: activity.actor }, `Inbox request from ${activity.actor}`); this.log.debug({ actor: activity.actor }, `Inbox request from ${activity.actor}`);
// :TODO: trace // :TODO: trace
const accountName = accountFromSelfUrl(activity.object); const accountName = accountFromSelfUrl(activity.object);
@ -346,8 +367,7 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
Actor.fromRemoteUrl(activity.actor, (err, actor) => { Actor.fromRemoteUrl(activity.actor, (err, actor) => {
if (err) { if (err) {
// :TODO: log, and probably should be inspecting |err| return this.webServer.internalServerError(resp, err);
return this.webServer.internalServerError(resp);
} }
const pubKey = actor.publicKey; const pubKey = actor.publicKey;
@ -427,7 +447,7 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
return callback(null); // just a warning return callback(null); // just a warning
} }
this.log.trace( this.log.info(
{ inbox: remoteActor.inbox }, { inbox: remoteActor.inbox },
'Remote server received our "Accept" successfully' 'Remote server received our "Accept" successfully'
); );
@ -463,8 +483,7 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
Actor.fromLocalUser(user, this.webServer, (err, actor) => { Actor.fromLocalUser(user, this.webServer, (err, actor) => {
if (err) { if (err) {
// :TODO: Log me return this.webServer.internalServerError(resp, err);
return this.webServer.internalServerError(resp);
} }
const body = JSON.stringify(actor); const body = JSON.stringify(actor);