Retro style default profile, constant cleanup, some DRY, etc.

This commit is contained in:
Bryan Ashby 2023-01-29 16:52:01 -07:00
parent 2a75d55b42
commit 3bdce81bdb
No known key found for this signature in database
GPG Key ID: C2C1B501E4EFD994
8 changed files with 270 additions and 93 deletions

View File

@ -1,5 +1,11 @@
const { localActorId } = require('./util'); const { localActorId } = require('./util');
const { WellKnownActivityTypes } = require('./const'); const {
ActivityStreamMediaType,
WellKnownActivityTypes,
WellKnownActivity,
WellKnownRecipientFields,
HttpSignatureSignHeaders,
} = require('./const');
const ActivityPubObject = require('./object'); const ActivityPubObject = require('./object');
const { Errors } = require('../enig_error'); const { Errors } = require('../enig_error');
const UserProps = require('../user_property'); const UserProps = require('../user_property');
@ -26,7 +32,7 @@ module.exports = class Activity extends ActivityPubObject {
static makeFollow(webServer, localActor, remoteActor) { static makeFollow(webServer, localActor, remoteActor) {
return new Activity({ return new Activity({
id: Activity.activityObjectId(webServer), id: Activity.activityObjectId(webServer),
type: 'Follow', type: WellKnownActivity.Follow,
actor: localActor, actor: localActor,
object: remoteActor.id, object: remoteActor.id,
}); });
@ -36,7 +42,7 @@ module.exports = class Activity extends ActivityPubObject {
static makeAccept(webServer, localActor, followRequest) { static makeAccept(webServer, localActor, followRequest) {
return new Activity({ return new Activity({
id: Activity.activityObjectId(webServer), id: Activity.activityObjectId(webServer),
type: 'Accept', type: WellKnownActivity.Accept,
actor: localActor, actor: localActor,
object: followRequest, // previous request Activity object: followRequest, // previous request Activity
}); });
@ -45,7 +51,7 @@ module.exports = class Activity extends ActivityPubObject {
static makeCreate(webServer, actor, obj) { static makeCreate(webServer, actor, obj) {
return new Activity({ return new Activity({
id: Activity.activityObjectId(webServer), id: Activity.activityObjectId(webServer),
type: 'Create', type: WellKnownActivity.Create,
actor, actor,
object: obj, object: obj,
}); });
@ -55,7 +61,7 @@ module.exports = class Activity extends ActivityPubObject {
const deleted = getISOTimestampString(); const deleted = getISOTimestampString();
return new Activity({ return new Activity({
id: obj.id, id: obj.id,
type: 'Tombstone', type: WellKnownActivity.Tombstone,
deleted, deleted,
published: deleted, published: deleted,
updated: deleted, updated: deleted,
@ -74,14 +80,13 @@ module.exports = class Activity extends ActivityPubObject {
const reqOpts = { const reqOpts = {
headers: { headers: {
'Content-Type': 'application/activity+json', 'Content-Type': ActivityStreamMediaType,
}, },
sign: { sign: {
// :TODO: Make a helper for this
key: privateKey, key: privateKey,
keyId: localActorId(webServer, fromUser) + '#main-key', keyId: localActorId(webServer, fromUser) + '#main-key',
authorizationHeaderName: 'Signature', authorizationHeaderName: 'Signature',
headers: ['(request-target)', 'host', 'date', 'digest', 'content-type'], headers: HttpSignatureSignHeaders,
}, },
}; };
@ -92,8 +97,7 @@ module.exports = class Activity extends ActivityPubObject {
recipientIds() { recipientIds() {
const ids = []; const ids = [];
// :TODO: bto, bcc? WellKnownRecipientFields.forEach(field => {
['to', 'cc', 'audience'].forEach(field => {
let v = this[field]; let v = this[field];
if (v) { if (v) {
if (!Array.isArray(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) { static activityObjectId(webServer) {

View File

@ -17,6 +17,7 @@ const { queryWebFinger } = require('../webfinger');
const EnigAssert = require('../enigma_assert'); const EnigAssert = require('../enigma_assert');
const ActivityPubSettings = require('./settings'); const ActivityPubSettings = require('./settings');
const ActivityPubObject = require('./object'); const ActivityPubObject = require('./object');
const { ActivityStreamMediaType } = require('./const');
const apDb = require('../database').dbs.activitypub; const apDb = require('../database').dbs.activitypub;
// deps // deps
@ -96,6 +97,12 @@ module.exports = class Actor extends ActivityPubObject {
'@context': [ '@context': [
ActivityStreamsContext, ActivityStreamsContext,
'https://w3id.org/security/v1', // :TODO: add support 'https://w3id.org/security/v1', // :TODO: add support
{
bbsPublicStats: {
'@id': 'bbs:bbsPublicStats',
'@type': '@id',
},
},
], ],
id: userActorId, id: userActorId,
type: 'Person', type: 'Person',
@ -122,6 +129,25 @@ module.exports = class Actor extends ActivityPubObject {
// value: 'Mateo@21:1/121', // 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'); addImage(obj, 'icon');
@ -190,7 +216,7 @@ module.exports = class Actor extends ActivityPubObject {
static _fromRemoteQuery(id, cb) { static _fromRemoteQuery(id, cb) {
const headers = { const headers = {
Accept: 'application/activity+json', Accept: ActivityStreamMediaType,
}; };
getJson(id, { headers }, (err, actor) => { getJson(id, { headers }, (err, actor) => {
@ -268,7 +294,7 @@ module.exports = class Actor extends ActivityPubObject {
} }
const activityLink = links.find(l => { 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) { if (!activityLink) {

View File

@ -1,5 +1,6 @@
exports.ActivityStreamsContext = 'https://www.w3.org/ns/activitystreams'; exports.ActivityStreamsContext = 'https://www.w3.org/ns/activitystreams';
exports.PublicCollectionId = 'https://www.w3.org/ns/activitystreams#Public'; exports.PublicCollectionId = 'https://www.w3.org/ns/activitystreams#Public';
exports.ActivityStreamMediaType = 'application/activity+json';
const WellKnownActivity = { const WellKnownActivity = {
Create: 'Create', Create: 'Create',
@ -13,8 +14,20 @@ const WellKnownActivity = {
Like: 'Like', Like: 'Like',
Announce: 'Announce', Announce: 'Announce',
Undo: 'Undo', Undo: 'Undo',
Tombstone: 'Tombstone',
}; };
exports.WellKnownActivity = WellKnownActivity; exports.WellKnownActivity = WellKnownActivity;
const WellKnownActivityTypes = Object.values(WellKnownActivity); const WellKnownActivityTypes = Object.values(WellKnownActivity);
exports.WellKnownActivityTypes = WellKnownActivityTypes; 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',
];

View File

@ -14,6 +14,7 @@ const paths = require('path');
const moment = require('moment'); const moment = require('moment');
const { striptags } = require('striptags'); const { striptags } = require('striptags');
const { encode, decode } = require('html-entities'); const { encode, decode } = require('html-entities');
const { isString } = require('lodash');
exports.ActivityStreamsContext = 'https://www.w3.org/ns/activitystreams'; exports.ActivityStreamsContext = 'https://www.w3.org/ns/activitystreams';
exports.isValidLink = isValidLink; exports.isValidLink = isValidLink;
@ -32,9 +33,9 @@ exports.userNameFromSubject = userNameFromSubject;
// profiles and 'self' requests without the // profiles and 'self' requests without the
// Accept: application/activity+json headers present // Accept: application/activity+json headers present
exports.DefaultProfileTemplate = ` exports.DefaultProfileTemplate = `
User information for: %USERNAME% User information for: %PREFERRED_USERNAME%
Real Name: %REAL_NAME% Name: %NAME%
Login Count: %LOGIN_COUNT% Login Count: %LOGIN_COUNT%
Affiliations: %AFFILIATIONS% Affiliations: %AFFILIATIONS%
Achievement Points: %ACHIEVEMENT_POINTS% Achievement Points: %ACHIEVEMENT_POINTS%
@ -102,8 +103,10 @@ function userFromActorId(actorId, cb) {
} }
function getUserProfileTemplatedBody( function getUserProfileTemplatedBody(
webServer,
templateFile, templateFile,
user, user,
userAsActor,
defaultTemplate, defaultTemplate,
defaultContentType, defaultContentType,
cb cb
@ -130,36 +133,53 @@ function getUserProfileTemplatedBody(
return callback(null, template, contentType); return callback(null, template, contentType);
}, },
(template, contentType, callback) => { (template, contentType, callback) => {
const up = (p, na = 'N/A') => { const val = v => {
return user.getProperty(p) || na; 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)) { if (moment.isDate(birthDate)) {
birthDate = moment(birthDate); birthDate = moment(birthDate);
} }
const varMap = { const varMap = {
USERNAME: user.username, ACTOR_OBJ: JSON.stringify(userAsActor),
REAL_NAME: user.getSanitizedName('real'), SUBJECT: `@${user.username}@${webServer.getDomain()}`,
SEX: up(UserProps.Sex), 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, BIRTHDATE: birthDate,
AGE: user.getAge(), AGE: user.getAge(),
LOCATION: up(UserProps.Location), LOCATION: user.getProperty(UserProps.Location),
AFFILIATIONS: up(UserProps.Affiliations), AFFILIATIONS: user.getProperty(UserProps.Affiliations),
EMAIL: up(UserProps.EmailAddress), EMAIL: user.getProperty(UserProps.EmailAddress),
WEB_ADDRESS: up(UserProps.WebAddress), WEB_ADDRESS: user.getProperty(UserProps.WebAddress),
ACCOUNT_CREATED: moment(user.getProperty(UserProps.AccountCreated)), ACCOUNT_CREATED: moment(user.getProperty(UserProps.AccountCreated)),
LAST_LOGIN: moment(user.getProperty(UserProps.LastLoginTs)), LAST_LOGIN: moment(user.getProperty(UserProps.LastLoginTs)),
LOGIN_COUNT: up(UserProps.LoginCount), LOGIN_COUNT: user.getPropertyAsNumber(UserProps.LoginCount),
ACHIEVEMENT_COUNT: up(UserProps.AchievementTotalCount, '0'), ACHIEVEMENT_COUNT: user.getPropertyAsNumber(
ACHIEVEMENT_POINTS: up(UserProps.AchievementTotalPoints, '0'), UserProps.AchievementTotalCount
),
ACHIEVEMENT_POINTS: user.getProperty(
UserProps.AchievementTotalPoints
),
BOARDNAME: Config().general.boardName, BOARDNAME: Config().general.boardName,
}; };
let body = template; let body = template;
_.each(varMap, (val, varName) => { _.each(varMap, (v, varName) => {
body = body.replace(new RegExp(`%${varName}%`, 'g'), val); body = body.replace(new RegExp(`%${varName}%`, 'g'), val(v));
}); });
return callback(null, body, contentType); return callback(null, body, contentType);

View File

@ -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' }) { accepted(resp, body = '', headers = { 'Content-Type:': 'text/html' }) {
resp.writeHead(202, 'Accepted', body ? headers : null); resp.writeHead(202, 'Accepted', body ? headers : null);
return resp.end(body); return resp.end(body);

View File

@ -6,6 +6,7 @@ const {
makeUserUrl, makeUserUrl,
localActorId, localActorId,
} = require('../../../activitypub/util'); } = require('../../../activitypub/util');
const { ActivityStreamMediaType } = require('../../../activitypub/const');
const Config = require('../../../config').get; const Config = require('../../../config').get;
const Activity = require('../../../activitypub/activity'); const Activity = require('../../../activitypub/activity');
const ActivityPubSettings = require('../../../activitypub/settings'); const ActivityPubSettings = require('../../../activitypub/settings');
@ -31,8 +32,6 @@ exports.moduleInfo = {
packageName: 'codes.l33t.enigma.web.handler.activitypub', packageName: 'codes.l33t.enigma.web.handler.activitypub',
}; };
const ActivityJsonMime = 'application/activity+json';
exports.getModule = class ActivityPubWebHandler extends WebHandlerModule { exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
constructor() { constructor() {
super(); super();
@ -149,7 +148,7 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
// Additionally, serve activity JSON if the proper 'Accept' header was sent // Additionally, serve activity JSON if the proper 'Accept' header was sent
const accept = req.headers['accept'].split(',').map(v => v.trim()) || ['*/*']; const accept = req.headers['accept'].split(',').map(v => v.trim()) || ['*/*'];
const headerValues = [ const headerValues = [
ActivityJsonMime, ActivityStreamMediaType,
'application/ld+json', 'application/ld+json',
'application/json', 'application/json',
]; ];
@ -166,11 +165,17 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
return this.webServer.resourceNotFound(resp); return this.webServer.resourceNotFound(resp);
} }
if (sendActor) { Actor.fromLocalUser(localUser, this.webServer, (err, localActor) => {
return this._selfAsActorHandler(localUser, req, resp); if (err) {
} else { return this.webServer.internalServerError(resp, err);
return this._standardSelfHandler(localUser, req, resp); }
}
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.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 body = JSON.stringify(collection);
const headers = { const headers = {
'Content-Type': ActivityJsonMime, 'Content-Type': ActivityStreamMediaType,
'Content-Length': body.length, 'Content-Length': body.length,
}; };
@ -700,30 +705,24 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
console.log(resp); console.log(resp);
} }
_selfAsActorHandler(user, req, resp) { _selfAsActorHandler(localUser, localActor, req, resp) {
this.log.trace( this.log.trace(
{ username: user.username }, { username: localUser.username },
`Serving ActivityPub Actor for "${user.username}"` `Serving ActivityPub Actor for "${localUser.username}"`
); );
Actor.fromLocalUser(user, this.webServer, (err, actor) => { const body = JSON.stringify(localActor);
if (err) {
return this.webServer.internalServerError(resp, err);
}
const body = JSON.stringify(actor); const headers = {
'Content-Type': ActivityStreamMediaType,
'Content-Length': body.length,
};
const headers = { resp.writeHead(200, headers);
'Content-Type': ActivityJsonMime, return resp.end(body);
'Content-Length': body.length,
};
resp.writeHead(200, headers);
return resp.end(body);
});
} }
_standardSelfHandler(user, req, resp) { _standardSelfHandler(localUser, localActor, req, resp) {
let templateFile = _.get( let templateFile = _.get(
Config(), Config(),
'contentServers.web.handlers.activityPub.selfTemplate' '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 // we'll fall back to the same default profile info as the WebFinger profile
getUserProfileTemplatedBody( getUserProfileTemplatedBody(
this.webServer,
templateFile, templateFile,
user, localUser,
localActor,
DefaultProfileTemplate, DefaultProfileTemplate,
'text/plain', 'text/plain',
(err, body, contentType) => { (err, body, contentType) => {

View File

@ -15,6 +15,7 @@ const ActivityPubSettings = require('../../../activitypub/settings');
// deps // deps
const _ = require('lodash'); const _ = require('lodash');
const Actor = require('../../../activitypub/actor');
exports.moduleInfo = { exports.moduleInfo = {
name: 'WebFinger', name: 'WebFinger',
@ -104,25 +105,33 @@ exports.getModule = class WebFingerWebHandler extends WebHandlerModule {
templateFile = this.webServer.resolveTemplatePath(templateFile); templateFile = this.webServer.resolveTemplatePath(templateFile);
} }
getUserProfileTemplatedBody( Actor.fromLocalUser(localUser, this.webServer, (err, localActor) => {
templateFile, if (err) {
localUser, return this.webServer.internalServerError(resp, err);
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);
} }
);
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);
}
);
});
}); });
} }

View File

@ -1,19 +1,118 @@
<!doctype html> <!DOCTYPE html>
<html class="no-js" lang="en"> <html class="no-js" lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>%USERNAME%</title> <title>%NAME%</title>
<meta name="description" content="Profile of %USERNAME% of %BOARDNAME%"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta http-equiv="X-UA-Compatible" content="ie=edge">
</head> <meta name="description" content="Profile of %PREFERRED_USERNAME% of %BOARDNAME%">
<body> </head>
<h1>%USERNAME% of %BOARDNAME%:</h1> <style>
<p> body {
<b>Real Name</b>: %REAL_NAME%<br> margin: 0 auto;
<b>Location</b>: %LOCATION%<br> padding: 0;
<b>Login Count</b> %LOGIN_COUNT%<br> background: #000;
<b>Affils</b>: %AFFILIATIONS%<br> }
<b>Account Since</b>: %ACCOUNT_CREATED%<br>
</p> .left {
</body> left: 25px;
}
.right {
right: 25px;
}
.center {
text-align: center;
}
.bottom {
position: absolute;
bottom: 25px;
}
#gradient {
background: #999955;
background-image: linear-gradient(#aa5500 20%, #aa0000 20%, #aa0000 40%, #00aa00 40%, #00aa00 60%, #00aaaa 60%, #00aaaa 80%, #aa00aa 80%);
margin: 0 auto;
margin-top: 100px;
width: 100%;
height: 150px;
}
#gradient:after {
content: "";
position: absolute;
background: #555555;
left: 50%;
margin-top: -67.5px;
margin-left: -270px;
padding-left: 20px;
border-radius: 5px;
width: 520px;
height: 275px;
z-index: -1;
}
#card {
position: absolute;
width: 450px;
height: 225px;
padding: 25px;
padding-top: 0;
padding-bottom: 0;
left: 50%;
top: 67.5px;
margin-left: -250px;
background: #aaaaaa;
box-shadow: -20px 0 35px -25px black, 20px 0 35px -25px black;
z-index: 5;
}
#card img {
width: 150px;
float: left;
border-radius: 5px;
margin-right: 20px;
-webkit-filter: sepia(1);
-moz-filter: sepia(1);
filter: sepia(1);
}
#card h2 {
font-family: courier;
color: #ff55ff;
background: #aa00aa;
margin: 0 auto;
padding: 0;
font-size: 15pt;
}
#card p {
font-family: courier;
color: #000000;
font-size: 13px;
}
#card span {
font-family: courier;
}
</style>
<body>
<div id="gradient"></div>
<div id="card">
<img src="%USER_ICON%" />
<h2>%NAME%</h2>
<p>
<b>Name</b>: %NAME% <br>
<b>Location</b>: %LOCATION% <br>
<b>Login Count</b>: %LOGIN_COUNT% <br>
<b>Last Login <b>: %LAST_LOGIN% <br>
<b>Affils</b>: %AFFILIATIONS% <br>
<b>Account Since</b>: %ACCOUNT_CREATED% <br>
</p>
<span class="left bottom">%SUBJECT%</span>
<span class="right bottom">%BOARDNAME%!</span>
</div>
</body>
</html> </html>