diff --git a/core/activitypub/actor.js b/core/activitypub/actor.js index 6e22c148..13d53dff 100644 --- a/core/activitypub/actor.js +++ b/core/activitypub/actor.js @@ -26,6 +26,7 @@ const mimeTypes = require('mime-types'); const { getJson } = require('../http_util.js'); const { getISOTimestampString } = require('../database.js'); const moment = require('moment'); +const paths = require('path'); const ActorCacheTTL = moment.duration(1, 'day'); @@ -82,7 +83,8 @@ module.exports = class Actor extends ActivityPubObject { const addImage = (o, t) => { const url = userSettings[t]; if (url) { - const mt = mimeTypes.contentType(url); + const fn = paths.basename(url); + const mt = mimeTypes.contentType(fn); if (mt) { o[t] = { mediaType: mt, diff --git a/core/activitypub/util.js b/core/activitypub/util.js index 3d89c042..7a0f0d8a 100644 --- a/core/activitypub/util.js +++ b/core/activitypub/util.js @@ -137,6 +137,9 @@ function getUserProfileTemplatedBody( if (isString(v)) { return v ? encode(v) : ''; } else { + if (isNaN(v)) { + return ''; + } return v ? v : 0; } }; @@ -171,7 +174,7 @@ function getUserProfileTemplatedBody( ACHIEVEMENT_COUNT: user.getPropertyAsNumber( UserProps.AchievementTotalCount ), - ACHIEVEMENT_POINTS: user.getProperty( + ACHIEVEMENT_POINTS: user.getPropertyAsNumber( UserProps.AchievementTotalPoints ), BOARDNAME: Config().general.boardName, diff --git a/core/config_default.js b/core/config_default.js index 27c3945b..e2752603 100644 --- a/core/config_default.js +++ b/core/config_default.js @@ -306,6 +306,9 @@ module.exports = () => { systemGeneral: { enabled: true, }, + nodeInfo2: { + enabled: true, + }, }, resetPassword: { diff --git a/core/servers/content/web_handlers/nodeinfo2.js b/core/servers/content/web_handlers/nodeinfo2.js new file mode 100644 index 00000000..f5c3cc2e --- /dev/null +++ b/core/servers/content/web_handlers/nodeinfo2.js @@ -0,0 +1,123 @@ +const WebHandlerModule = require('../../../web_handler_module'); +const { Errors } = require('../../../enig_error'); +const EngiAssert = require('../../../enigma_assert'); +const Config = require('../../../config').get; +const packageJson = require('../../../../package.json'); +const StatLog = require('../../../stat_log'); +const SysProps = require('../../../system_property'); +const SysLogKeys = require('../../../system_log'); + +// deps +const moment = require('moment'); +const async = require('async'); + +exports.moduleInfo = { + name: 'NodeInfo2', + desc: 'A NodeInfo2 Handler implementing https://github.com/jaywink/nodeinfo2', + author: 'NuSkooler', + packageName: 'codes.l33t.enigma.web.handler.nodeinfo2', +}; + +exports.getModule = class NodeInfo2WebHadnler extends WebHandlerModule { + constructor() { + super(); + } + + init(webServer, cb) { + // we rely on the web server + this.webServer = webServer; + EngiAssert(webServer, 'NodeInfo2 Web Handler init without webServer'); + + this.log = webServer.logger().child({ webHandler: 'NodeInfo2' }); + + const domain = this.webServer.getDomain(); + if (!domain) { + return cb(Errors.UnexpectedState('Web server does not have "domain" set')); + } + + this.webServer.addRoute({ + method: 'GET', + path: /^\/\.well-known\/x-nodeinfo2$/, + handler: this._nodeInfo2Handler.bind(this), + }); + + return cb(null); + } + + _nodeInfo2Handler(req, resp) { + this.log.info({ url: req.url }, 'Serving NodeInfo2 request'); + + this._getNodeInfo(nodeInfo => { + const body = JSON.stringify(nodeInfo); + const headers = { + 'Content-Type': 'application/json', + 'Content-Length': body.length, + }; + + resp.writeHead(200, headers); + return resp.end(body); + }); + } + + _getNodeInfo(cb) { + // https://github.com/jaywink/nodeinfo2/tree/master/schemas/1.0 + const config = Config(); + const nodeInfo = { + version: '1.0', + server: { + baseUrl: this.webServer.baseUrl(), + name: config.general.boardName, + software: 'ENiGMA½ Bulletin Board Software', + version: packageJson.version, + }, + // :TODO: Only list what's enabled + protocols: ['telnet', 'ssh', 'gopher', 'nntp', 'ws', 'activitypub'], + services: { + inbound: [], + outbound: [''], + }, + openRegistrations: !config.general.closedSystem, + usage: { + users: { + total: StatLog.getSystemStatNum(SysProps.TotalUserCount) || 1, + // others fetched dynamically below + }, + + // :TODO: pop with local message + // select count() from message_meta where meta_name='local_from_user_id'; + localPosts: 0, + }, + }; + + const setActive = (since, name, next) => { + const filter = { + logName: SysLogKeys.UserLoginHistory, + resultType: 'count', + dateNewer: moment().subtract(moment.duration(since, 'days')), + }; + StatLog.findSystemLogEntries(filter, (err, count) => { + if (!err) { + nodeInfo.usage[name] = count; + } + return next(null); + }); + }; + + async.series( + [ + callback => { + return setActive(180, 'activeHalfyear', callback); + }, + callback => { + return setActive(30, 'activeMonth', callback); + }, + callback => { + return setActive(7, 'activeWeek', callback); + }, + ], + () => { + return cb(nodeInfo); + } + ); + } +}; diff --git a/core/stat_log.js b/core/stat_log.js index f2d57bd0..923abe7a 100644 --- a/core/stat_log.js +++ b/core/stat_log.js @@ -8,6 +8,7 @@ const SysProps = require('./system_property.js'); const UserProps = require('./user_property'); const Message = require('./message'); const { getActiveConnections, AllConnections } = require('./client_connections'); +const Log = require('./logger').log; // deps const _ = require('lodash'); @@ -349,6 +350,7 @@ class StatLog { // - resultType: 'obj' | 'count' (default='obj') // - limit: Limit returned results // - date: exact date to filter against + // - dateNewer: Entries newer than this value // - order: 'timestamp' | 'timestamp_asc' | 'timestamp_desc' | 'random' // (default='timestamp') // @@ -402,7 +404,9 @@ class StatLog { this.setNonPersistentSystemStat(SysProps.SystemLoadStats, loadStats); }) .catch(err => { - // :TODO: log me + if (err) { + Log.err({ error: err.message }, 'Error refreshing system stats'); + } }); } @@ -509,6 +513,11 @@ class StatLog { sql += ` AND DATE(timestamp, "localtime") = DATE("${filter.date.format( 'YYYY-MM-DD' )}")`; + } else if (filter.dateNewer) { + filter.dateNewer = moment(filter.dateNewer); + sql += ` AND DATE(timestamp, "localtime") > DATE("${filter.dateNewer.format( + 'YYYY-MM-DD' + )}")`; } if ('count' !== filter.resultType) {