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 { 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) {

View File

@ -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) {

View File

@ -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',
];

View File

@ -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);

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' }) {
resp.writeHead(202, 'Accepted', body ? headers : null);
return resp.end(body);

View File

@ -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(actor);
const body = JSON.stringify(localActor);
const headers = {
'Content-Type': ActivityJsonMime,
'Content-Type': ActivityStreamMediaType,
'Content-Length': body.length,
};
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) => {

View File

@ -15,6 +15,7 @@ const ActivityPubSettings = require('../../../activitypub/settings');
// deps
const _ = require('lodash');
const Actor = require('../../../activitypub/actor');
exports.moduleInfo = {
name: 'WebFinger',
@ -104,9 +105,16 @@ exports.getModule = class WebFingerWebHandler extends WebHandlerModule {
templateFile = this.webServer.resolveTemplatePath(templateFile);
}
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) => {
@ -124,6 +132,7 @@ exports.getModule = class WebFingerWebHandler extends WebHandlerModule {
}
);
});
});
}
_webFingerRequestHandler(req, resp) {

View File

@ -1,19 +1,118 @@
<!doctype html>
<!DOCTYPE html>
<html class="no-js" lang="en">
<head>
<meta charset="utf-8">
<title>%USERNAME%</title>
<meta name="description" content="Profile of %USERNAME% of %BOARDNAME%">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>%NAME%</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<meta name="description" content="Profile of %PREFERRED_USERNAME% of %BOARDNAME%">
</head>
<style>
body {
margin: 0 auto;
padding: 0;
background: #000;
}
.left {
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>
<h1>%USERNAME% of %BOARDNAME%:</h1>
<div id="gradient"></div>
<div id="card">
<img src="%USER_ICON%" />
<h2>%NAME%</h2>
<p>
<b>Real Name</b>: %REAL_NAME%<br>
<b>Name</b>: %NAME% <br>
<b>Location</b>: %LOCATION% <br>
<b>Login Count</b> %LOGIN_COUNT%<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>