diff --git a/core/activitypub/actor.js b/core/activitypub/actor.js index a04fcaf1..721a5e4d 100644 --- a/core/activitypub/actor.js +++ b/core/activitypub/actor.js @@ -18,9 +18,9 @@ const ActivityPubSettings = require('./settings'); // deps const _ = require('lodash'); -const https = require('https'); const isString = require('lodash/isString'); const mimeTypes = require('mime-types'); +const { getJson } = require('../http_util.js'); // https://www.w3.org/TR/activitypub/#actor-objects module.exports = class Actor { @@ -141,40 +141,18 @@ module.exports = class Actor { }; // :TODO: use getJson() - - https.get(url, { headers }, res => { - if (res.statusCode !== 200) { - return cb(Errors.Invalid(`Bad HTTP status code: ${res.statusCode}`)); + getJson(url, { headers }, (err, actor) => { + if (err) { + return cb(err); } - const contentType = res.headers['content-type']; - if ( - !_.isString(contentType) || - !contentType.startsWith('application/activity+json') - ) { - return cb(Errors.Invalid(`Invalid Content-Type: ${contentType}`)); + actor = new Actor(actor); + + if (!actor.isValid()) { + return cb(Errors.Invalid('Invalid Actor')); } - res.setEncoding('utf8'); - 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); - }); + return cb(null, actor); }); } diff --git a/core/activitypub/collection.js b/core/activitypub/collection.js index 084d5fd1..1aa9d617 100644 --- a/core/activitypub/collection.js +++ b/core/activitypub/collection.js @@ -2,13 +2,42 @@ const { makeUserUrl } = require('./util'); const ActivityPubObject = require('./object'); const apDb = require('../database').dbs.activitypub; const { getISOTimestampString } = require('../database'); -const { isString } = require('lodash'); + +const { isString, get } = require('lodash'); module.exports = class Collection extends ActivityPubObject { constructor(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) { // :TODD: |includePrivate| handling const followersUrl = @@ -24,12 +53,22 @@ module.exports = class Collection extends ActivityPubObject { return cb(err); } - const obj = { - id: followersUrl, - type: 'OrderedCollection', - first: `${followersUrl}?page=1`, - totalItems: row.count, - }; + let obj; + if (row.count > 0) { + obj = { + id: followersUrl, + type: 'OrderedCollection', + first: `${followersUrl}?page=1`, + totalItems: row.count, + }; + } else { + obj = { + id: followersUrl, + type: 'OrderedCollection', + totalItems: 0, + orderedItems: [], + }; + } return cb(null, new Collection(obj)); } @@ -48,7 +87,8 @@ module.exports = class Collection extends ActivityPubObject { return cb(err); } - if (mapper) { + entries = entries || []; + if (mapper && entries.length > 0) { 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) { if (!isString(entry)) { entry = JSON.stringify(entry); } apDb.run( - `INSERT INTO collection_entry (name, timestamp, user_id, entry_json) - VALUES (?, ?, ?, ?);`, + `INSERT OR IGNORE INTO collection_entry (name, timestamp, user_id, entry_json) + VALUES (?, ?, ?, ?);`, [name, getISOTimestampString(), owningUser.userId, entry], function res(err) { // non-arrow for 'this' scope + if (err) { + return cb(err); + } return cb(err, this.lastID); } ); } - - static addFollower(owningUser, followingActor, cb) { - return Collection.addToCollection('followers', owningUser, followingActor, cb); - } }; diff --git a/core/database.js b/core/database.js index 45b9f0b5..2b08daba 100644 --- a/core/database.js +++ b/core/database.js @@ -542,8 +542,13 @@ dbs.message.run( ); dbs.activitypub.run( - `CREATE INDEX IF NOT EXISTS collection_entry_unique_index0 - ON collection_entry (name, user_id, json_extract(entry_json, '$.id'))` + `CREATE INDEX IF NOT EXISTS collection_entry_by_user_index0 + 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); diff --git a/core/servers/content/web.js b/core/servers/content/web.js index b24968d8..5524635c 100644 --- a/core/servers/content/web.js +++ b/core/servers/content/web.js @@ -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( resp, 500, diff --git a/core/servers/content/web_handlers/activitypub.js b/core/servers/content/web_handlers/activitypub.js index ea0004d8..abca78f2 100644 --- a/core/servers/content/web_handlers/activitypub.js +++ b/core/servers/content/web_handlers/activitypub.js @@ -64,7 +64,25 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule { this.webServer.addRoute({ method: 'GET', 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 @@ -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/ _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 const signature = this._parseAndValidateSignature(req); @@ -208,8 +271,7 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule { Activity.fromOutboxEntries(user, this.webServer, (err, activity) => { if (err) { - // :TODO: LOG ME - return this.webServer.internalServerError(resp); + return this.webServer.internalServerError(resp, err); } const body = JSON.stringify(activity); @@ -234,49 +296,8 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule { } _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.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); - }); - }); + this.log.debug({ url: req.url }, 'Request for "followers"'); + return this._getCollectionHandler('followers', req, resp); } _parseAndValidateSignature(req) { @@ -331,7 +352,7 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule { } _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 const accountName = accountFromSelfUrl(activity.object); @@ -346,8 +367,7 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule { Actor.fromRemoteUrl(activity.actor, (err, actor) => { if (err) { - // :TODO: log, and probably should be inspecting |err| - return this.webServer.internalServerError(resp); + return this.webServer.internalServerError(resp, err); } const pubKey = actor.publicKey; @@ -427,7 +447,7 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule { return callback(null); // just a warning } - this.log.trace( + this.log.info( { inbox: remoteActor.inbox }, 'Remote server received our "Accept" successfully' ); @@ -463,8 +483,7 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule { Actor.fromLocalUser(user, this.webServer, (err, actor) => { if (err) { - // :TODO: Log me - return this.webServer.internalServerError(resp); + return this.webServer.internalServerError(resp, err); } const body = JSON.stringify(actor);