Hand back a profile for self

This commit is contained in:
Bryan Ashby 2023-01-04 20:29:18 -07:00
parent d4f74447ec
commit 5055337eff
No known key found for this signature in database
GPG Key ID: C2C1B501E4EFD994
4 changed files with 195 additions and 123 deletions

View File

@ -3,10 +3,32 @@ const User = require('./user');
const { Errors } = require('./enig_error'); const { Errors } = require('./enig_error');
const UserProps = require('./user_property'); 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.makeUserUrl = makeUserUrl;
exports.webFingerProfileUrl = webFingerProfileUrl; exports.webFingerProfileUrl = webFingerProfileUrl;
exports.selfUrl = selfUrl; exports.selfUrl = selfUrl;
exports.userFromAccount = userFromAccount; 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) { function makeUserUrl(webServer, user, relPrefix) {
return webServer.buildUrl( 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);
}
);
}

View File

@ -317,8 +317,8 @@ exports.getModule = class WebServerModule extends ServerModule {
req.url.substr(req.url.lastIndexOf('/', 1)), req.url.substr(req.url.lastIndexOf('/', 1)),
tryFile tryFile
); );
const filePath = this.resolveStaticPath(fileName);
const filePath = this.resolveStaticPath(fileName);
fs.stat(filePath, (err, stats) => { fs.stat(filePath, (err, stats) => {
if (err || !stats.isFile()) { if (err || !stats.isFile()) {
return nextTryFile(null, false); return nextTryFile(null, false);

View File

@ -4,9 +4,15 @@ const {
webFingerProfileUrl, webFingerProfileUrl,
selfUrl, selfUrl,
userFromAccount, userFromAccount,
getUserProfileTemplatedBody,
DefaultProfileTemplate,
} = require('../../../activitypub_util'); } = require('../../../activitypub_util');
const UserProps = require('../../../user_property'); const UserProps = require('../../../user_property');
const { Errors } = require('../../../enig_error'); const { Errors } = require('../../../enig_error');
const Config = require('../../../config').get;
// deps
const _ = require('lodash');
exports.moduleInfo = { exports.moduleInfo = {
name: 'ActivityPub', name: 'ActivityPub',
@ -36,15 +42,6 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
} }
_selfUrlRequestHandler(req, resp) { _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 url = new URL(req.url, `https://${req.headers.host}`);
const accountName = url.pathname.substring(url.pathname.lastIndexOf('/') + 1); const accountName = url.pathname.substring(url.pathname.lastIndexOf('/') + 1);
@ -53,42 +50,77 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
return this._notFound(resp); return this._notFound(resp);
} }
const body = JSON.stringify({ const accept = req.headers['accept'] || '*/*';
'@context': [ if (accept === 'application/activity+json') {
'https://www.w3.org/ns/activitystreams', return this._selfAsActorHandler(user, req, resp);
'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 return this._standardSelfHandler(user, req, resp);
});
const headers = {
'Content-Type': 'application/activity+json',
'Content-Length': body.length,
};
resp.writeHead(200, headers);
return resp.end(body);
}); });
} }
_standardSelfHandler(req, resp) { _selfAsActorHandler(user, req, resp) {
// :TODO: this should also be their profile page?! Perhaps that should also be shared... const body = JSON.stringify({
return this._notFound(resp); '@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) { _notFound(resp) {

View File

@ -1,22 +1,17 @@
const WebHandlerModule = require('../../../web_handler_module'); const WebHandlerModule = require('../../../web_handler_module');
const Config = require('../../../config').get; const Config = require('../../../config').get;
const { Errors, ErrorReasons } = require('../../../enig_error'); const { Errors } = require('../../../enig_error');
const { WellKnownLocations } = require('../web'); const { WellKnownLocations } = require('../web');
const { const {
selfUrl, selfUrl,
webFingerProfileUrl, webFingerProfileUrl,
userFromAccount, userFromAccount,
getUserProfileTemplatedBody,
DefaultProfileTemplate,
} = require('../../../activitypub_util'); } = require('../../../activitypub_util');
const _ = require('lodash'); const _ = require('lodash');
const User = require('../../../user');
const UserProps = require('../../../user_property');
const Log = require('../../../logger').log; 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 = { exports.moduleInfo = {
name: 'WebFinger', name: 'WebFinger',
@ -25,16 +20,6 @@ exports.moduleInfo = {
packageName: 'codes.l33t.enigma.web.handler.webfinger', 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 // WebFinger: https://www.rfc-editor.org/rfc/rfc7033
// //
@ -117,70 +102,33 @@ exports.getModule = class WebFingerWebHandler extends WebHandlerModule {
return this._notFound(resp); return this._notFound(resp);
} }
this._getProfileTemplate((template, mimeType) => { let templateFile = _.get(
const up = (p, na = 'N/A') => { Config(),
return user.getProperty(p) || na; 'contentServers.web.handlers.webFinger.profileTemplate'
}; );
if (templateFile) {
let birthDate = up(UserProps.Birthdate); templateFile = this.webServer.resolveTemplatePath(templateFile);
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');
} }
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);
}
);
}); });
} }