From 3bdce81bdb0308a9ba4f91bfdb11aa661601a0c6 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 29 Jan 2023 16:52:01 -0700 Subject: [PATCH] Retro style default profile, constant cleanup, some DRY, etc. --- core/activitypub/activity.js | 26 ++-- core/activitypub/actor.js | 30 +++- core/activitypub/const.js | 13 ++ core/activitypub/util.js | 54 ++++--- core/servers/content/web.js | 5 + .../content/web_handlers/activitypub.js | 57 ++++---- .../servers/content/web_handlers/webfinger.js | 45 +++--- www/wf/profile.template.html | 133 +++++++++++++++--- 8 files changed, 270 insertions(+), 93 deletions(-) diff --git a/core/activitypub/activity.js b/core/activitypub/activity.js index c66c3e44..07a25e14 100644 --- a/core/activitypub/activity.js +++ b/core/activitypub/activity.js @@ -1,5 +1,11 @@ const { localActorId } = require('./util'); -const { WellKnownActivityTypes } = require('./const'); +const { + ActivityStreamMediaType, + WellKnownActivityTypes, + WellKnownActivity, + WellKnownRecipientFields, + HttpSignatureSignHeaders, +} = require('./const'); const ActivityPubObject = require('./object'); const { Errors } = require('../enig_error'); const UserProps = require('../user_property'); @@ -26,7 +32,7 @@ module.exports = class Activity extends ActivityPubObject { static makeFollow(webServer, localActor, remoteActor) { return new Activity({ id: Activity.activityObjectId(webServer), - type: 'Follow', + type: WellKnownActivity.Follow, actor: localActor, object: remoteActor.id, }); @@ -36,7 +42,7 @@ module.exports = class Activity extends ActivityPubObject { static makeAccept(webServer, localActor, followRequest) { return new Activity({ id: Activity.activityObjectId(webServer), - type: 'Accept', + type: WellKnownActivity.Accept, actor: localActor, object: followRequest, // previous request Activity }); @@ -45,7 +51,7 @@ module.exports = class Activity extends ActivityPubObject { static makeCreate(webServer, actor, obj) { return new Activity({ id: Activity.activityObjectId(webServer), - type: 'Create', + type: WellKnownActivity.Create, actor, object: obj, }); @@ -55,7 +61,7 @@ module.exports = class Activity extends ActivityPubObject { const deleted = getISOTimestampString(); return new Activity({ id: obj.id, - type: 'Tombstone', + type: WellKnownActivity.Tombstone, deleted, published: deleted, updated: deleted, @@ -74,14 +80,13 @@ module.exports = class Activity extends ActivityPubObject { const reqOpts = { headers: { - 'Content-Type': 'application/activity+json', + 'Content-Type': ActivityStreamMediaType, }, sign: { - // :TODO: Make a helper for this key: privateKey, keyId: localActorId(webServer, fromUser) + '#main-key', authorizationHeaderName: 'Signature', - headers: ['(request-target)', 'host', 'date', 'digest', 'content-type'], + headers: HttpSignatureSignHeaders, }, }; @@ -92,8 +97,7 @@ module.exports = class Activity extends ActivityPubObject { recipientIds() { const ids = []; - // :TODO: bto, bcc? - ['to', 'cc', 'audience'].forEach(field => { + WellKnownRecipientFields.forEach(field => { let v = this[field]; if (v) { if (!Array.isArray(v)) { @@ -103,7 +107,7 @@ module.exports = class Activity extends ActivityPubObject { } }); - return ids; + return Array.from(new Set(ids)); } static activityObjectId(webServer) { diff --git a/core/activitypub/actor.js b/core/activitypub/actor.js index 4e56e4cf..6e22c148 100644 --- a/core/activitypub/actor.js +++ b/core/activitypub/actor.js @@ -17,6 +17,7 @@ const { queryWebFinger } = require('../webfinger'); const EnigAssert = require('../enigma_assert'); const ActivityPubSettings = require('./settings'); const ActivityPubObject = require('./object'); +const { ActivityStreamMediaType } = require('./const'); const apDb = require('../database').dbs.activitypub; // deps @@ -96,6 +97,12 @@ module.exports = class Actor extends ActivityPubObject { '@context': [ ActivityStreamsContext, 'https://w3id.org/security/v1', // :TODO: add support + { + bbsPublicStats: { + '@id': 'bbs:bbsPublicStats', + '@type': '@id', + }, + }, ], id: userActorId, type: 'Person', @@ -122,6 +129,25 @@ module.exports = class Actor extends ActivityPubObject { // value: 'Mateo@21:1/121', // }, // ], + bbsPublicStats: { + affiliations: user.getProperty(UserProps.Affiliations) || '', + lastLogin: user.getProperty(UserProps.LastLoginTs), + loginCount: user.getPropertyAsNumber(UserProps.LoginCount), + joined: user.getProperty(UserProps.AccountCreated), + postCount: user.getPropertyAsNumber(UserProps.MessagePostCount), + doorCount: user.getPropertyAsNumber(UserProps.DoorRunTotalCount), + doorMinute: user.getPropertyAsNumber(UserProps.DoorRunTotalMinutes), + achievementCount: user.getPropertyAsNumber( + UserProps.AchievementTotalCount + ), + achievementPoints: user.getPropertyAsNumber( + UserProps.AchievementTotalPoints + ), + uploadCount: user.getPropertyAsNumber(UserProps.FileUlTotalCount), + downloadCount: user.getPropertyAsNumber(UserProps.FileDlTotalCount), + uploadBytes: user.getPropertyAsNumber(UserProps.FileUlTotalBytes), + downloadBytes: user.getPropertyAsNumber(UserProps.FileDlTotalBytes), + }, }; addImage(obj, 'icon'); @@ -190,7 +216,7 @@ module.exports = class Actor extends ActivityPubObject { static _fromRemoteQuery(id, cb) { const headers = { - Accept: 'application/activity+json', + Accept: ActivityStreamMediaType, }; getJson(id, { headers }, (err, actor) => { @@ -268,7 +294,7 @@ module.exports = class Actor extends ActivityPubObject { } const activityLink = links.find(l => { - return l.type === 'application/activity+json' && l.href?.length > 0; + return l.type === ActivityStreamMediaType && l.href?.length > 0; }); if (!activityLink) { diff --git a/core/activitypub/const.js b/core/activitypub/const.js index 61e005f2..56221a03 100644 --- a/core/activitypub/const.js +++ b/core/activitypub/const.js @@ -1,5 +1,6 @@ exports.ActivityStreamsContext = 'https://www.w3.org/ns/activitystreams'; exports.PublicCollectionId = 'https://www.w3.org/ns/activitystreams#Public'; +exports.ActivityStreamMediaType = 'application/activity+json'; const WellKnownActivity = { Create: 'Create', @@ -13,8 +14,20 @@ const WellKnownActivity = { Like: 'Like', Announce: 'Announce', Undo: 'Undo', + Tombstone: 'Tombstone', }; exports.WellKnownActivity = WellKnownActivity; const WellKnownActivityTypes = Object.values(WellKnownActivity); exports.WellKnownActivityTypes = WellKnownActivityTypes; + +exports.WellKnownRecipientFields = ['audience', 'bcc', 'bto', 'cc', 'to']; + +// Signatures utilized in HTTP signature generation +exports.HttpSignatureSignHeaders = [ + '(request-target)', + 'host', + 'date', + 'digest', + 'content-type', +]; diff --git a/core/activitypub/util.js b/core/activitypub/util.js index e2738d48..3d89c042 100644 --- a/core/activitypub/util.js +++ b/core/activitypub/util.js @@ -14,6 +14,7 @@ const paths = require('path'); const moment = require('moment'); const { striptags } = require('striptags'); const { encode, decode } = require('html-entities'); +const { isString } = require('lodash'); exports.ActivityStreamsContext = 'https://www.w3.org/ns/activitystreams'; exports.isValidLink = isValidLink; @@ -32,9 +33,9 @@ exports.userNameFromSubject = userNameFromSubject; // profiles and 'self' requests without the // Accept: application/activity+json headers present exports.DefaultProfileTemplate = ` -User information for: %USERNAME% +User information for: %PREFERRED_USERNAME% -Real Name: %REAL_NAME% +Name: %NAME% Login Count: %LOGIN_COUNT% Affiliations: %AFFILIATIONS% Achievement Points: %ACHIEVEMENT_POINTS% @@ -102,8 +103,10 @@ function userFromActorId(actorId, cb) { } function getUserProfileTemplatedBody( + webServer, templateFile, user, + userAsActor, defaultTemplate, defaultContentType, cb @@ -130,36 +133,53 @@ function getUserProfileTemplatedBody( return callback(null, template, contentType); }, (template, contentType, callback) => { - const up = (p, na = 'N/A') => { - return user.getProperty(p) || na; + const val = v => { + if (isString(v)) { + return v ? encode(v) : ''; + } else { + return v ? v : 0; + } }; - let birthDate = up(UserProps.Birthdate); + let birthDate = val(user.getProperty(UserProps.Birthdate)); if (moment.isDate(birthDate)) { birthDate = moment(birthDate); } const varMap = { - USERNAME: user.username, - REAL_NAME: user.getSanitizedName('real'), - SEX: up(UserProps.Sex), + ACTOR_OBJ: JSON.stringify(userAsActor), + SUBJECT: `@${user.username}@${webServer.getDomain()}`, + INBOX: userAsActor.inbox, + SHARED_INBOX: userAsActor.endpoints.sharedInbox, + OUTBOX: userAsActor.outbox, + FOLLOWERS: userAsActor.followers, + FOLLOWING: userAsActor.following, + USER_ICON: userAsActor.icon.url, + USER_IMAGE: userAsActor.image.url, + PREFERRED_USERNAME: userAsActor.preferredUsername, + NAME: userAsActor.name, + SEX: user.getProperty(UserProps.Sex), BIRTHDATE: birthDate, AGE: user.getAge(), - LOCATION: up(UserProps.Location), - AFFILIATIONS: up(UserProps.Affiliations), - EMAIL: up(UserProps.EmailAddress), - WEB_ADDRESS: up(UserProps.WebAddress), + LOCATION: user.getProperty(UserProps.Location), + AFFILIATIONS: user.getProperty(UserProps.Affiliations), + EMAIL: user.getProperty(UserProps.EmailAddress), + WEB_ADDRESS: user.getProperty(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'), + LOGIN_COUNT: user.getPropertyAsNumber(UserProps.LoginCount), + ACHIEVEMENT_COUNT: user.getPropertyAsNumber( + UserProps.AchievementTotalCount + ), + ACHIEVEMENT_POINTS: user.getProperty( + UserProps.AchievementTotalPoints + ), BOARDNAME: Config().general.boardName, }; let body = template; - _.each(varMap, (val, varName) => { - body = body.replace(new RegExp(`%${varName}%`, 'g'), val); + _.each(varMap, (v, varName) => { + body = body.replace(new RegExp(`%${varName}%`, 'g'), val(v)); }); return callback(null, body, contentType); diff --git a/core/servers/content/web.js b/core/servers/content/web.js index 6f39bd11..713b7a4a 100644 --- a/core/servers/content/web.js +++ b/core/servers/content/web.js @@ -324,6 +324,11 @@ exports.getModule = class WebServerModule extends ServerModule { }); } + created(resp, body = '', headers = { 'Content-Type:': 'text/html' }) { + resp.writeHead(201, 'Created', body ? headers : null); + return resp.end(body); + } + accepted(resp, body = '', headers = { 'Content-Type:': 'text/html' }) { resp.writeHead(202, 'Accepted', body ? headers : null); return resp.end(body); diff --git a/core/servers/content/web_handlers/activitypub.js b/core/servers/content/web_handlers/activitypub.js index 3b3238b3..386a7bac 100644 --- a/core/servers/content/web_handlers/activitypub.js +++ b/core/servers/content/web_handlers/activitypub.js @@ -6,6 +6,7 @@ const { makeUserUrl, localActorId, } = require('../../../activitypub/util'); +const { ActivityStreamMediaType } = require('../../../activitypub/const'); const Config = require('../../../config').get; const Activity = require('../../../activitypub/activity'); const ActivityPubSettings = require('../../../activitypub/settings'); @@ -31,8 +32,6 @@ exports.moduleInfo = { packageName: 'codes.l33t.enigma.web.handler.activitypub', }; -const ActivityJsonMime = 'application/activity+json'; - exports.getModule = class ActivityPubWebHandler extends WebHandlerModule { constructor() { super(); @@ -149,7 +148,7 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule { // Additionally, serve activity JSON if the proper 'Accept' header was sent const accept = req.headers['accept'].split(',').map(v => v.trim()) || ['*/*']; const headerValues = [ - ActivityJsonMime, + ActivityStreamMediaType, 'application/ld+json', 'application/json', ]; @@ -166,11 +165,17 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule { return this.webServer.resourceNotFound(resp); } - if (sendActor) { - return this._selfAsActorHandler(localUser, req, resp); - } else { - return this._standardSelfHandler(localUser, req, resp); - } + Actor.fromLocalUser(localUser, this.webServer, (err, localActor) => { + if (err) { + return this.webServer.internalServerError(resp, err); + } + + if (sendActor) { + return this._selfAsActorHandler(localUser, localActor, req, resp); + } else { + return this._standardSelfHandler(localUser, localActor, req, resp); + } + }); }); } @@ -341,7 +346,7 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule { return this.webServer.internalServerError(resp, err); } - return this.webServer.accepted(resp); + return this.webServer.created(resp); } ); } @@ -409,7 +414,7 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule { const body = JSON.stringify(collection); const headers = { - 'Content-Type': ActivityJsonMime, + 'Content-Type': ActivityStreamMediaType, 'Content-Length': body.length, }; @@ -700,30 +705,24 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule { console.log(resp); } - _selfAsActorHandler(user, req, resp) { + _selfAsActorHandler(localUser, localActor, req, resp) { this.log.trace( - { username: user.username }, - `Serving ActivityPub Actor for "${user.username}"` + { username: localUser.username }, + `Serving ActivityPub Actor for "${localUser.username}"` ); - Actor.fromLocalUser(user, this.webServer, (err, actor) => { - if (err) { - return this.webServer.internalServerError(resp, err); - } + const body = JSON.stringify(localActor); - const body = JSON.stringify(actor); + const headers = { + 'Content-Type': ActivityStreamMediaType, + 'Content-Length': body.length, + }; - const headers = { - 'Content-Type': ActivityJsonMime, - 'Content-Length': body.length, - }; - - resp.writeHead(200, headers); - return resp.end(body); - }); + resp.writeHead(200, headers); + return resp.end(body); } - _standardSelfHandler(user, req, resp) { + _standardSelfHandler(localUser, localActor, req, resp) { let templateFile = _.get( Config(), 'contentServers.web.handlers.activityPub.selfTemplate' @@ -734,8 +733,10 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule { // we'll fall back to the same default profile info as the WebFinger profile getUserProfileTemplatedBody( + this.webServer, templateFile, - user, + localUser, + localActor, DefaultProfileTemplate, 'text/plain', (err, body, contentType) => { diff --git a/core/servers/content/web_handlers/webfinger.js b/core/servers/content/web_handlers/webfinger.js index 8b952112..a101e260 100644 --- a/core/servers/content/web_handlers/webfinger.js +++ b/core/servers/content/web_handlers/webfinger.js @@ -15,6 +15,7 @@ const ActivityPubSettings = require('../../../activitypub/settings'); // deps const _ = require('lodash'); +const Actor = require('../../../activitypub/actor'); exports.moduleInfo = { name: 'WebFinger', @@ -104,25 +105,33 @@ exports.getModule = class WebFingerWebHandler extends WebHandlerModule { templateFile = this.webServer.resolveTemplatePath(templateFile); } - getUserProfileTemplatedBody( - templateFile, - localUser, - DefaultProfileTemplate, - 'text/plain', - (err, body, contentType) => { - if (err) { - return this.webServer.resourceNotFound(resp); - } - - const headers = { - 'Content-Type': contentType, - 'Content-Length': body.length, - }; - - resp.writeHead(200, headers); - return resp.end(body); + Actor.fromLocalUser(localUser, this.webServer, (err, localActor) => { + if (err) { + return this.webServer.internalServerError(resp, err); } - ); + + getUserProfileTemplatedBody( + this.webServer, + templateFile, + localUser, + localActor, + DefaultProfileTemplate, + 'text/plain', + (err, body, contentType) => { + if (err) { + return this.webServer.resourceNotFound(resp); + } + + const headers = { + 'Content-Type': contentType, + 'Content-Length': body.length, + }; + + resp.writeHead(200, headers); + return resp.end(body); + } + ); + }); }); } diff --git a/www/wf/profile.template.html b/www/wf/profile.template.html index 2912ad7b..909c89ae 100644 --- a/www/wf/profile.template.html +++ b/www/wf/profile.template.html @@ -1,19 +1,118 @@ - + - - - %USERNAME% - - - - -

%USERNAME% of %BOARDNAME%:

-

- Real Name: %REAL_NAME%
- Location: %LOCATION%
- Login Count %LOGIN_COUNT%
- Affils: %AFFILIATIONS%
- Account Since: %ACCOUNT_CREATED%
-

- + + + %NAME% + + + + + + +
+
+ +

%NAME%

+

+ Name: %NAME%
+ Location: %LOCATION%
+ Login Count: %LOGIN_COUNT%
+ Last Login : %LAST_LOGIN%
+ Affils: %AFFILIATIONS%
+ Account Since: %ACCOUNT_CREATED%
+

+ %SUBJECT% + %BOARDNAME%! +
+ \ No newline at end of file