diff --git a/core/activitypub_activity.js b/core/activitypub_activity.js index f2f2d25b..5aa07167 100644 --- a/core/activitypub_activity.js +++ b/core/activitypub_activity.js @@ -1,7 +1,10 @@ const { isString, isObject } = require('lodash'); +const { v4: UUIDv4 } = require('uuid'); +const { ActivityStreamsContext } = require('./activitypub_util'); module.exports = class Activity { constructor(obj) { + this['@context'] = ActivityStreamsContext; Object.assign(this, obj); } @@ -28,7 +31,7 @@ module.exports = class Activity { isValid() { if ( - this['@context'] !== 'https://www.w3.org/ns/activitystreams' || + this['@context'] !== ActivityStreamsContext || !isString(this.id) || !isString(this.actor) || (!isString(this.object) && !isObject(this.object)) || @@ -41,4 +44,19 @@ module.exports = class Activity { return true; } + + // https://www.w3.org/TR/activitypub/#accept-activity-inbox + static makeAccept(webServer, localActor, followRequest, id = null) { + id = id || webServer.buildUrl(`/${UUIDv4()}`); + + return new Activity({ + type: 'Accept', + actor: localActor, + object: followRequest, // previous request Activity + }); + } + + sendTo(actorUrl, cb) { + // :TODO: https send |this| to actorUrl + } }; diff --git a/core/activitypub_actor.js b/core/activitypub_actor.js index fa7a65fd..c9dacf85 100644 --- a/core/activitypub_actor.js +++ b/core/activitypub_actor.js @@ -11,6 +11,7 @@ const { makeUserUrl, selfUrl, isValidLink, + ActivityStreamsContext, } = require('./activitypub_util'); const Log = require('./logger').log; @@ -25,10 +26,11 @@ const isString = require('lodash/isString'); // https://www.w3.org/TR/activitypub/#actor-objects module.exports = class Actor { constructor(obj) { + this['@context'] = [ActivityStreamsContext]; + if (obj) { Object.assign(this, obj); } else { - this['@context'] = ['https://www.w3.org/ns/activitystreams']; this.id = ''; this.type = ''; this.inbox = ''; @@ -41,7 +43,7 @@ module.exports = class Actor { isValid() { if ( !Array.isArray(this['@context']) || - this['@context'][0] !== 'https://www.w3.org/ns/activitystreams' + this['@context'][0] !== ActivityStreamsContext ) { return false; } @@ -66,7 +68,7 @@ module.exports = class Actor { const obj = { '@context': [ - 'https://www.w3.org/ns/activitystreams', + ActivityStreamsContext, 'https://w3id.org/security/v1', // :TODO: add support ], id: userSelfUrl, diff --git a/core/activitypub_settings.js b/core/activitypub_settings.js new file mode 100644 index 00000000..f98ca650 --- /dev/null +++ b/core/activitypub_settings.js @@ -0,0 +1,42 @@ +const { selfUrl } = require('./activitypub_util'); +const UserProps = require('./user_property'); + +module.exports = class ActivityPubSettings { + constructor(obj) { + this.enabled = true; // :TODO: fetch from +op config default + this.manuallyApproveFollowers = false; + this.hideSocialGraph = false; // followers, following + this.showRealName = false; + this.imageUrl = ''; + this.iconUrl = ''; + + if (obj) { + Object.assign(this, obj); + } + } + + static fromUser(user) { + if (!user.activityPubSettings) { + const settingsProp = user.getProperty(UserProps.ActivityPubSettings); + let settings; + try { + const parsed = JSON.parse(settingsProp); + settings = new ActivityPubSettings(parsed); + } catch (e) { + settings = new ActivityPubSettings(); + } + + user.activityPubSettings = settings; + } + + return user.activityPubSettings; + } + + persistToUserProperties(user, cb = null) { + return user.persistProperty( + UserProps.ActivityPubSettings, + JSON.stringify(this), + cb + ); + } +}; diff --git a/core/activitypub_util.js b/core/activitypub_util.js index 339464c6..667390c4 100644 --- a/core/activitypub_util.js +++ b/core/activitypub_util.js @@ -2,6 +2,7 @@ const { WellKnownLocations } = require('./servers/content/web'); const User = require('./user'); const { Errors, ErrorReasons } = require('./enig_error'); const UserProps = require('./user_property'); +const ActivityPubSettings = require('./activitypub_settings'); // deps const _ = require('lodash'); @@ -11,6 +12,7 @@ const fs = require('graceful-fs'); const paths = require('path'); const moment = require('moment'); +exports.ActivityStreamsContext = 'https://www.w3.org/ns/activitystreams'; exports.isValidLink = isValidLink; exports.makeUserUrl = makeUserUrl; exports.webFingerProfileUrl = webFingerProfileUrl; @@ -52,6 +54,7 @@ function selfUrl(webServer, user) { function accountFromSelfUrl(url) { // https://some.l33t.enigma.board/_enig/ap/users/Masto -> Masto + // :TODO: take webServer, and just take path-to-users.length +1 return url.substring(url.lastIndexOf('/') + 1); } @@ -78,6 +81,11 @@ function userFromAccount(accountName, cb) { 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); }); }); diff --git a/core/servers/content/web_handlers/activitypub.js b/core/servers/content/web_handlers/activitypub.js index 968d3678..06cbb9f4 100644 --- a/core/servers/content/web_handlers/activitypub.js +++ b/core/servers/content/web_handlers/activitypub.js @@ -1,16 +1,13 @@ const WebHandlerModule = require('../../../web_handler_module'); const { - makeUserUrl, - webFingerProfileUrl, - selfUrl, userFromAccount, getUserProfileTemplatedBody, DefaultProfileTemplate, accountFromSelfUrl, } = require('../../../activitypub_util'); -const UserProps = require('../../../user_property'); const Config = require('../../../config').get; const Activity = require('../../../activitypub_activity'); +const ActivityPubSettings = require('../../../activitypub_settings'); // deps const _ = require('lodash'); @@ -46,7 +43,6 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule { this.webServer.addRoute({ method: 'POST', - //inbox: makeUserUrl(this.webServer, user, '/ap/users/') + '/inbox', path: /^\/_enig\/ap\/users\/.+\/inbox$/, handler: this._inboxPostHandler.bind(this), }); @@ -208,6 +204,31 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule { // :TODO: return OK and kick off a async job of persisting and sending and 'Accepted' + // + // If the user blindly accepts Followers, we can persist + // and send an 'Accept' now. Otherwise, we need to queue this + // request for the user to review and decide what to do with + // at a later time. + // + const activityPubSettings = ActivityPubSettings.fromUser(user); + if (!activityPubSettings.manuallyApproveFollowers) { + // + // :TODO: Implement the queue + Actor.fromLocalUser(user, this.webServer, (err, localActor) => { + if (err) { + // :TODO: + return; + } + + const accept = Activity.makeAccept( + this.webServer, + localActor, + activity + ); + console.log(accept); + }); + } + resp.writeHead(200, { 'Content-Type': 'text/html' }); return resp.end(''); }); diff --git a/core/user_property.js b/core/user_property.js index d173b0a4..f52c8b7b 100644 --- a/core/user_property.js +++ b/core/user_property.js @@ -69,4 +69,6 @@ module.exports = { PublicKeyMain: 'public_key_main_rsa_pem', // RSA public key for user PrivateKeyMain: 'private_key_main_rsa_pem', // RSA private key (corresponding to PublicKeyMain) + + ActivityPubSettings: 'activity_pub_settings', // JSON object (above); see ActivityPubSettings in activitypub_settings.js };