From 5055337eff4ecf63feef882925cedcca213bb860 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 4 Jan 2023 20:29:18 -0700 Subject: [PATCH] Hand back a profile for self --- core/activitypub_util.js | 92 ++++++++++++++ core/servers/content/web.js | 2 +- .../content/web_handlers/activitypub.js | 114 +++++++++++------- .../servers/content/web_handlers/webfinger.js | 110 +++++------------ 4 files changed, 195 insertions(+), 123 deletions(-) diff --git a/core/activitypub_util.js b/core/activitypub_util.js index 90ee210a..1ccb27ac 100644 --- a/core/activitypub_util.js +++ b/core/activitypub_util.js @@ -3,10 +3,32 @@ const User = require('./user'); const { Errors } = require('./enig_error'); const UserProps = require('./user_property'); +// deps +const _ = require('lodash'); +const mimeTypes = require('mime-types'); +const waterfall = require('async/waterfall'); +const fs = require('graceful-fs'); +const paths = require('path'); +const moment = require('moment'); + exports.makeUserUrl = makeUserUrl; exports.webFingerProfileUrl = webFingerProfileUrl; exports.selfUrl = selfUrl; exports.userFromAccount = userFromAccount; +exports.getUserProfileTemplatedBody = getUserProfileTemplatedBody; + +// :TODO: more info in default +// this profile template is the *default* for both WebFinger +// profiles and 'self' requests without the +// Accept: application/activity+json headers present +exports.DefaultProfileTemplate = ` +User information for: %USERNAME% + +Real Name: %REAL_NAME% +Login Count: %LOGIN_COUNT% +Affiliations: %AFFILIATIONS% +Achievement Points: %ACHIEVEMENT_POINTS% +`; function makeUserUrl(webServer, user, relPrefix) { return webServer.buildUrl( @@ -45,3 +67,73 @@ function userFromAccount(accountName, cb) { }); }); } + +function getUserProfileTemplatedBody( + templateFile, + user, + defaultTemplate, + defaultContentType, + cb +) { + const Log = require('./logger').log; + const Config = require('./config').get; + + waterfall( + [ + callback => { + return fs.readFile(templateFile || '', 'utf8', (err, template) => { + return callback(null, template); + }); + }, + (template, callback) => { + if (!template) { + if (templateFile) { + Log.warn(`Failed to load profile template "${templateFile}"`); + } + return callback(null, defaultTemplate, defaultContentType); + } + + const contentType = mimeTypes.contentType(paths.basename(templateFile)); + return callback(null, template, contentType); + }, + (template, contentType, callback) => { + const up = (p, na = 'N/A') => { + return user.getProperty(p) || na; + }; + + let birthDate = up(UserProps.Birthdate); + if (moment.isDate(birthDate)) { + birthDate = moment(birthDate); + } + + const varMap = { + USERNAME: user.username, + REAL_NAME: user.getSanitizedName('real'), + SEX: up(UserProps.Sex), + BIRTHDATE: birthDate, + AGE: user.getAge(), + LOCATION: up(UserProps.Location), + AFFILIATIONS: up(UserProps.Affiliations), + EMAIL: up(UserProps.EmailAddress), + WEB_ADDRESS: up(UserProps.WebAddress), + ACCOUNT_CREATED: moment(user.getProperty(UserProps.AccountCreated)), + LAST_LOGIN: moment(user.getProperty(UserProps.LastLoginTs)), + LOGIN_COUNT: up(UserProps.LoginCount), + ACHIEVEMENT_COUNT: up(UserProps.AchievementTotalCount, '0'), + ACHIEVEMENT_POINTS: up(UserProps.AchievementTotalPoints, '0'), + BOARDNAME: Config().general.boardName, + }; + + let body = template; + _.each(varMap, (val, varName) => { + body = body.replace(new RegExp(`%${varName}%`, 'g'), val); + }); + + return callback(null, body, contentType); + }, + ], + (err, data, contentType) => { + return cb(err, data, contentType); + } + ); +} diff --git a/core/servers/content/web.js b/core/servers/content/web.js index 90fea57c..6631ea5c 100644 --- a/core/servers/content/web.js +++ b/core/servers/content/web.js @@ -317,8 +317,8 @@ exports.getModule = class WebServerModule extends ServerModule { req.url.substr(req.url.lastIndexOf('/', 1)), tryFile ); - const filePath = this.resolveStaticPath(fileName); + const filePath = this.resolveStaticPath(fileName); fs.stat(filePath, (err, stats) => { if (err || !stats.isFile()) { return nextTryFile(null, false); diff --git a/core/servers/content/web_handlers/activitypub.js b/core/servers/content/web_handlers/activitypub.js index a8c53efd..470adecc 100644 --- a/core/servers/content/web_handlers/activitypub.js +++ b/core/servers/content/web_handlers/activitypub.js @@ -4,9 +4,15 @@ const { webFingerProfileUrl, selfUrl, userFromAccount, + getUserProfileTemplatedBody, + DefaultProfileTemplate, } = require('../../../activitypub_util'); const UserProps = require('../../../user_property'); const { Errors } = require('../../../enig_error'); +const Config = require('../../../config').get; + +// deps +const _ = require('lodash'); exports.moduleInfo = { name: 'ActivityPub', @@ -36,15 +42,6 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule { } _selfUrlRequestHandler(req, resp) { - const accept = req.headers['accept'] || '*/*'; - if (accept === 'application/activity+json') { - return this._selfAsActorHandler(req, resp); - } - - return this._standardSelfHandler(req, resp); - } - - _selfAsActorHandler(req, resp) { const url = new URL(req.url, `https://${req.headers.host}`); const accountName = url.pathname.substring(url.pathname.lastIndexOf('/') + 1); @@ -53,42 +50,77 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule { return this._notFound(resp); } - const body = JSON.stringify({ - '@context': [ - 'https://www.w3.org/ns/activitystreams', - 'https://w3id.org/security/v1', - ], - id: selfUrl(this.webServer, user), - type: 'Person', - preferredUsername: user.username, - name: user.getSanitizedName('real'), - endpoints: { - sharedInbox: 'TODO', - }, - inbox: makeUserUrl(this.webServer, user, '/ap/users') + '/outbox', - outbox: makeUserUrl(this.webServer, user, '/ap/users') + '/inbox', - followers: makeUserUrl(this.webServer, user, '/ap/users') + '/followers', - following: makeUserUrl(this.webServer, user, '/ap/users') + '/following', - summary: user.getProperty(UserProps.AutoSignature) || '', - url: webFingerProfileUrl(this.webServer, user), - publicKey: {}, + const accept = req.headers['accept'] || '*/*'; + if (accept === 'application/activity+json') { + return this._selfAsActorHandler(user, req, resp); + } - // :TODO: we can start to define BBS related stuff with the community perhaps - }); - - const headers = { - 'Content-Type': 'application/activity+json', - 'Content-Length': body.length, - }; - - resp.writeHead(200, headers); - return resp.end(body); + return this._standardSelfHandler(user, req, resp); }); } - _standardSelfHandler(req, resp) { - // :TODO: this should also be their profile page?! Perhaps that should also be shared... - return this._notFound(resp); + _selfAsActorHandler(user, req, resp) { + const body = JSON.stringify({ + '@context': [ + 'https://www.w3.org/ns/activitystreams', + 'https://w3id.org/security/v1', + ], + id: selfUrl(this.webServer, user), + type: 'Person', + preferredUsername: user.username, + name: user.getSanitizedName('real'), + endpoints: { + sharedInbox: 'TODO', + }, + inbox: makeUserUrl(this.webServer, user, '/ap/users') + '/outbox', + outbox: makeUserUrl(this.webServer, user, '/ap/users') + '/inbox', + followers: makeUserUrl(this.webServer, user, '/ap/users') + '/followers', + following: makeUserUrl(this.webServer, user, '/ap/users') + '/following', + summary: user.getProperty(UserProps.AutoSignature) || '', + url: webFingerProfileUrl(this.webServer, user), + publicKey: {}, + + // :TODO: we can start to define BBS related stuff with the community perhaps + }); + + const headers = { + 'Content-Type': 'application/activity+json', + 'Content-Length': body.length, + }; + + resp.writeHead(200, headers); + return resp.end(body); + } + + _standardSelfHandler(user, req, resp) { + let templateFile = _.get( + Config(), + 'contentServers.web.handlers.activityPub.selfTemplate' + ); + if (templateFile) { + templateFile = this.webServer.resolveTemplatePath(templateFile); + } + + // we'll fall back to the same default profile info as the WebFinger profile + getUserProfileTemplatedBody( + templateFile, + user, + DefaultProfileTemplate, + 'text/plain', + (err, body, contentType) => { + if (err) { + return this._notFound(resp); + } + + const headers = { + 'Content-Type': contentType, + 'Content-Length': body.length, + }; + + resp.writeHead(200, headers); + return resp.end(body); + } + ); } _notFound(resp) { diff --git a/core/servers/content/web_handlers/webfinger.js b/core/servers/content/web_handlers/webfinger.js index 3f83da9a..7e7f9f1e 100644 --- a/core/servers/content/web_handlers/webfinger.js +++ b/core/servers/content/web_handlers/webfinger.js @@ -1,22 +1,17 @@ const WebHandlerModule = require('../../../web_handler_module'); const Config = require('../../../config').get; -const { Errors, ErrorReasons } = require('../../../enig_error'); +const { Errors } = require('../../../enig_error'); const { WellKnownLocations } = require('../web'); const { selfUrl, webFingerProfileUrl, userFromAccount, + getUserProfileTemplatedBody, + DefaultProfileTemplate, } = require('../../../activitypub_util'); const _ = require('lodash'); -const User = require('../../../user'); -const UserProps = require('../../../user_property'); const Log = require('../../../logger').log; -const mimeTypes = require('mime-types'); - -const fs = require('graceful-fs'); -const paths = require('path'); -const moment = require('moment'); exports.moduleInfo = { name: 'WebFinger', @@ -25,16 +20,6 @@ exports.moduleInfo = { packageName: 'codes.l33t.enigma.web.handler.webfinger', }; -// :TODO: more info in default -const DefaultProfileTemplate = ` -User information for: %USERNAME% - -Real Name: %REAL_NAME% -Login Count: %LOGIN_COUNT% -Affiliations: %AFFILIATIONS% -Achievement Points: %ACHIEVEMENT_POINTS% -`; - // // WebFinger: https://www.rfc-editor.org/rfc/rfc7033 // @@ -117,70 +102,33 @@ exports.getModule = class WebFingerWebHandler extends WebHandlerModule { return this._notFound(resp); } - this._getProfileTemplate((template, mimeType) => { - const up = (p, na = 'N/A') => { - return user.getProperty(p) || na; - }; - - let birthDate = up(UserProps.Birthdate); - if (moment.isDate(birthDate)) { - birthDate = moment(birthDate); - } - - const varMap = { - USERNAME: user.username, - REAL_NAME: user.getSanitizedName('real'), - SEX: up(UserProps.Sex), - BIRTHDATE: birthDate, - AGE: user.getAge(), - LOCATION: up(UserProps.Location), - AFFILIATIONS: up(UserProps.Affiliations), - EMAIL: up(UserProps.EmailAddress), - WEB_ADDRESS: up(UserProps.WebAddress), - ACCOUNT_CREATED: moment(user.getProperty(UserProps.AccountCreated)), - LAST_LOGIN: moment(user.getProperty(UserProps.LastLoginTs)), - LOGIN_COUNT: up(UserProps.LoginCount), - ACHIEVEMENT_COUNT: up(UserProps.AchievementTotalCount, '0'), - ACHIEVEMENT_POINTS: up(UserProps.AchievementTotalPoints, '0'), - BOARDNAME: Config().general.boardName, - }; - - let body = template; - _.each(varMap, (val, varName) => { - body = body.replace(new RegExp(`%${varName}%`, 'g'), val); - }); - - const headers = { - 'Content-Type': mimeType, - 'Content-Length': body.length, - }; - - resp.writeHead(200, headers); - return resp.end(body); - }); - }); - } - - _getProfileTemplate(cb) { - let templateFile = _.get( - Config(), - 'contentServers.web.handlers.webFinger.profileTemplate' - ); - if (templateFile) { - templateFile = this.webServer.resolveTemplatePath(templateFile); - } - fs.readFile(templateFile || '', 'utf8', (err, data) => { - if (err) { - if (templateFile) { - Log.warn( - { error: err.message }, - `Failed to load profile template "${templateFile}"` - ); - } - - return cb(DefaultProfileTemplate, 'text/plain'); + let templateFile = _.get( + Config(), + 'contentServers.web.handlers.webFinger.profileTemplate' + ); + if (templateFile) { + templateFile = this.webServer.resolveTemplatePath(templateFile); } - return cb(data, mimeTypes.contentType(paths.basename(templateFile))); + + getUserProfileTemplatedBody( + templateFile, + user, + DefaultProfileTemplate, + 'text/plain', + (err, body, contentType) => { + if (err) { + return this._notFound(resp); + } + + const headers = { + 'Content-Type': contentType, + 'Content-Length': body.length, + }; + + resp.writeHead(200, headers); + return resp.end(body); + } + ); }); }