2023-01-13 06:19:52 +00:00
|
|
|
const { WellKnownLocations } = require('../servers/content/web');
|
|
|
|
const User = require('../user');
|
|
|
|
const { Errors, ErrorReasons } = require('../enig_error');
|
|
|
|
const UserProps = require('../user_property');
|
|
|
|
const ActivityPubSettings = require('./settings');
|
2023-01-26 05:22:45 +00:00
|
|
|
const { stripAnsiControlCodes } = require('../string_util');
|
2023-01-03 22:10:39 +00:00
|
|
|
|
2023-01-05 03:29:18 +00:00
|
|
|
// 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');
|
2023-01-25 04:40:12 +00:00
|
|
|
const { striptags } = require('striptags');
|
2023-01-26 05:22:45 +00:00
|
|
|
const { encode, decode } = require('html-entities');
|
2023-01-29 23:52:01 +00:00
|
|
|
const { isString } = require('lodash');
|
2023-01-05 03:29:18 +00:00
|
|
|
|
2023-01-09 00:11:49 +00:00
|
|
|
exports.ActivityStreamsContext = 'https://www.w3.org/ns/activitystreams';
|
2023-01-08 20:18:50 +00:00
|
|
|
exports.isValidLink = isValidLink;
|
2023-01-22 20:51:32 +00:00
|
|
|
exports.makeSharedInboxUrl = makeSharedInboxUrl;
|
2023-01-04 03:32:09 +00:00
|
|
|
exports.makeUserUrl = makeUserUrl;
|
|
|
|
exports.webFingerProfileUrl = webFingerProfileUrl;
|
2023-01-28 18:55:31 +00:00
|
|
|
exports.localActorId = localActorId;
|
|
|
|
exports.userFromActorId = userFromActorId;
|
2023-01-05 03:29:18 +00:00
|
|
|
exports.getUserProfileTemplatedBody = getUserProfileTemplatedBody;
|
2023-01-12 05:37:09 +00:00
|
|
|
exports.messageBodyToHtml = messageBodyToHtml;
|
2023-01-25 04:40:12 +00:00
|
|
|
exports.htmlToMessageBody = htmlToMessageBody;
|
2023-01-26 01:41:47 +00:00
|
|
|
exports.userNameFromSubject = userNameFromSubject;
|
2023-01-05 03:29:18 +00:00
|
|
|
|
|
|
|
// :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 = `
|
2023-01-29 23:52:01 +00:00
|
|
|
User information for: %PREFERRED_USERNAME%
|
2023-01-05 03:29:18 +00:00
|
|
|
|
2023-01-29 23:52:01 +00:00
|
|
|
Name: %NAME%
|
2023-01-05 03:29:18 +00:00
|
|
|
Login Count: %LOGIN_COUNT%
|
|
|
|
Affiliations: %AFFILIATIONS%
|
|
|
|
Achievement Points: %ACHIEVEMENT_POINTS%
|
|
|
|
`;
|
2023-01-03 22:10:39 +00:00
|
|
|
|
2023-01-08 20:18:50 +00:00
|
|
|
function isValidLink(l) {
|
|
|
|
return /^https?:\/\/.+$/.test(l);
|
|
|
|
}
|
|
|
|
|
2023-01-22 20:51:32 +00:00
|
|
|
function makeSharedInboxUrl(webServer) {
|
|
|
|
return webServer.buildUrl(WellKnownLocations.Internal + '/ap/shared-inbox');
|
|
|
|
}
|
|
|
|
|
2023-01-04 03:32:09 +00:00
|
|
|
function makeUserUrl(webServer, user, relPrefix) {
|
2023-01-03 22:10:39 +00:00
|
|
|
return webServer.buildUrl(
|
|
|
|
WellKnownLocations.Internal + `${relPrefix}${user.username}`
|
|
|
|
);
|
|
|
|
}
|
2023-01-04 03:32:09 +00:00
|
|
|
|
|
|
|
function webFingerProfileUrl(webServer, user) {
|
|
|
|
return webServer.buildUrl(WellKnownLocations.Internal + `/wf/@${user.username}`);
|
|
|
|
}
|
|
|
|
|
2023-01-28 18:55:31 +00:00
|
|
|
function localActorId(webServer, user) {
|
2023-01-04 03:32:09 +00:00
|
|
|
return makeUserUrl(webServer, user, '/ap/users/');
|
|
|
|
}
|
|
|
|
|
2023-01-28 18:55:31 +00:00
|
|
|
function userFromActorId(actorId, cb) {
|
|
|
|
User.getUserIdsWithProperty(UserProps.ActivityPubActorId, actorId, (err, userId) => {
|
2023-01-04 03:32:09 +00:00
|
|
|
if (err) {
|
|
|
|
return cb(err);
|
|
|
|
}
|
|
|
|
|
2023-01-28 18:55:31 +00:00
|
|
|
// must only be 0 or 1
|
|
|
|
if (!Array.isArray(userId) || userId.length !== 1) {
|
|
|
|
return cb(
|
|
|
|
Errors.DoesNotExist(
|
|
|
|
`No user with property '${UserProps.ActivityPubActorId}' of ${actorId}`
|
|
|
|
)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
userId = userId[0];
|
2023-01-04 03:32:09 +00:00
|
|
|
User.getUser(userId, (err, user) => {
|
|
|
|
if (err) {
|
|
|
|
return cb(err);
|
|
|
|
}
|
|
|
|
|
|
|
|
const accountStatus = user.getPropertyAsNumber(UserProps.AccountStatus);
|
|
|
|
if (
|
|
|
|
User.AccountStatus.disabled == accountStatus ||
|
2023-01-07 01:05:11 +00:00
|
|
|
User.AccountStatus.inactive == accountStatus
|
2023-01-04 03:32:09 +00:00
|
|
|
) {
|
|
|
|
return cb(Errors.AccessDenied('Account disabled', ErrorReasons.Disabled));
|
|
|
|
}
|
|
|
|
|
2023-01-09 00:11:49 +00:00
|
|
|
const activityPubSettings = ActivityPubSettings.fromUser(user);
|
|
|
|
if (!activityPubSettings.enabled) {
|
|
|
|
return cb(Errors.AccessDenied('ActivityPub is not enabled for user'));
|
|
|
|
}
|
|
|
|
|
2023-01-04 03:32:09 +00:00
|
|
|
return cb(null, user);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
2023-01-05 03:29:18 +00:00
|
|
|
|
|
|
|
function getUserProfileTemplatedBody(
|
2023-01-29 23:52:01 +00:00
|
|
|
webServer,
|
2023-01-05 03:29:18 +00:00
|
|
|
templateFile,
|
|
|
|
user,
|
2023-01-29 23:52:01 +00:00
|
|
|
userAsActor,
|
2023-01-05 03:29:18 +00:00
|
|
|
defaultTemplate,
|
|
|
|
defaultContentType,
|
|
|
|
cb
|
|
|
|
) {
|
2023-01-13 19:26:12 +00:00
|
|
|
const Log = require('../logger').log;
|
|
|
|
const Config = require('../config').get;
|
2023-01-05 03:29:18 +00:00
|
|
|
|
|
|
|
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) => {
|
2023-01-29 23:52:01 +00:00
|
|
|
const val = v => {
|
|
|
|
if (isString(v)) {
|
|
|
|
return v ? encode(v) : '';
|
|
|
|
} else {
|
2023-01-30 19:30:36 +00:00
|
|
|
if (isNaN(v)) {
|
|
|
|
return '';
|
|
|
|
}
|
2023-01-29 23:52:01 +00:00
|
|
|
return v ? v : 0;
|
|
|
|
}
|
2023-01-05 03:29:18 +00:00
|
|
|
};
|
|
|
|
|
2023-01-29 23:52:01 +00:00
|
|
|
let birthDate = val(user.getProperty(UserProps.Birthdate));
|
2023-01-05 03:29:18 +00:00
|
|
|
if (moment.isDate(birthDate)) {
|
|
|
|
birthDate = moment(birthDate);
|
|
|
|
}
|
|
|
|
|
|
|
|
const varMap = {
|
2023-01-29 23:52:01 +00:00
|
|
|
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),
|
2023-01-05 03:29:18 +00:00
|
|
|
BIRTHDATE: birthDate,
|
|
|
|
AGE: user.getAge(),
|
2023-01-29 23:52:01 +00:00
|
|
|
LOCATION: user.getProperty(UserProps.Location),
|
|
|
|
AFFILIATIONS: user.getProperty(UserProps.Affiliations),
|
|
|
|
EMAIL: user.getProperty(UserProps.EmailAddress),
|
|
|
|
WEB_ADDRESS: user.getProperty(UserProps.WebAddress),
|
2023-01-05 03:29:18 +00:00
|
|
|
ACCOUNT_CREATED: moment(user.getProperty(UserProps.AccountCreated)),
|
|
|
|
LAST_LOGIN: moment(user.getProperty(UserProps.LastLoginTs)),
|
2023-01-29 23:52:01 +00:00
|
|
|
LOGIN_COUNT: user.getPropertyAsNumber(UserProps.LoginCount),
|
|
|
|
ACHIEVEMENT_COUNT: user.getPropertyAsNumber(
|
|
|
|
UserProps.AchievementTotalCount
|
|
|
|
),
|
2023-01-30 19:30:36 +00:00
|
|
|
ACHIEVEMENT_POINTS: user.getPropertyAsNumber(
|
2023-01-29 23:52:01 +00:00
|
|
|
UserProps.AchievementTotalPoints
|
|
|
|
),
|
2023-01-05 03:29:18 +00:00
|
|
|
BOARDNAME: Config().general.boardName,
|
|
|
|
};
|
|
|
|
|
|
|
|
let body = template;
|
2023-01-29 23:52:01 +00:00
|
|
|
_.each(varMap, (v, varName) => {
|
|
|
|
body = body.replace(new RegExp(`%${varName}%`, 'g'), val(v));
|
2023-01-05 03:29:18 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
return callback(null, body, contentType);
|
|
|
|
},
|
|
|
|
],
|
|
|
|
(err, data, contentType) => {
|
|
|
|
return cb(err, data, contentType);
|
|
|
|
}
|
|
|
|
);
|
|
|
|
}
|
2023-01-12 05:37:09 +00:00
|
|
|
|
|
|
|
//
|
|
|
|
// Apply very basic HTML to a message following
|
|
|
|
// Mastodon's supported tags of 'p', 'br', 'a', and 'span':
|
2023-01-26 05:22:45 +00:00
|
|
|
// - https://docs.joinmastodon.org/spec/activitypub/#sanitization
|
|
|
|
// - https://blog.joinmastodon.org/2018/06/how-to-implement-a-basic-activitypub-server/
|
2023-01-12 05:37:09 +00:00
|
|
|
//
|
2023-01-26 05:22:45 +00:00
|
|
|
// :TODO: https://docs.joinmastodon.org/spec/microformats/
|
2023-01-12 05:37:09 +00:00
|
|
|
function messageBodyToHtml(body) {
|
2023-01-26 05:22:45 +00:00
|
|
|
body = encode(stripAnsiControlCodes(body), { mode: 'nonAsciiPrintable' }).replace(
|
|
|
|
/\r?\n/g,
|
|
|
|
'<br>'
|
|
|
|
);
|
|
|
|
|
|
|
|
return `<p>${body}</p>`;
|
2023-01-12 05:37:09 +00:00
|
|
|
}
|
2023-01-25 04:40:12 +00:00
|
|
|
|
|
|
|
function htmlToMessageBody(html) {
|
2023-01-26 05:22:45 +00:00
|
|
|
// <br>, </br>, and <br/> -> \r\n
|
|
|
|
html = html.replace(/<\/?br?\/?>/g, '\r\n');
|
|
|
|
return striptags(decode(html));
|
2023-01-25 04:40:12 +00:00
|
|
|
}
|
2023-01-26 01:41:47 +00:00
|
|
|
|
|
|
|
function userNameFromSubject(subject) {
|
|
|
|
return subject.replace(/^acct:(.+)$/, '$1');
|
|
|
|
}
|