diff --git a/core/activitypub_activity.js b/core/activitypub_activity.js
index 5aa07167..3eca0c58 100644
--- a/core/activitypub_activity.js
+++ b/core/activitypub_activity.js
@@ -1,6 +1,20 @@
const { isString, isObject } = require('lodash');
const { v4: UUIDv4 } = require('uuid');
-const { ActivityStreamsContext } = require('./activitypub_util');
+const {
+ ActivityStreamsContext,
+ messageBodyToHtml,
+ selfUrl,
+} = require('./activitypub_util');
+const { Errors } = require('./enig_error');
+const User = require('./user');
+const Actor = require('./activitypub_actor');
+const { getISOTimestampString } = require('./database');
+const UserProps = require('./user_property');
+const { postJson } = require('./http_util');
+
+// deps
+const async = require('async');
+const _ = require('lodash');
module.exports = class Activity {
constructor(obj) {
@@ -47,7 +61,7 @@ module.exports = class Activity {
// https://www.w3.org/TR/activitypub/#accept-activity-inbox
static makeAccept(webServer, localActor, followRequest, id = null) {
- id = id || webServer.buildUrl(`/${UUIDv4()}`);
+ id = id || Activity._makeId(webServer, '/accept');
return new Activity({
type: 'Accept',
@@ -56,7 +70,131 @@ module.exports = class Activity {
});
}
- sendTo(actorUrl, cb) {
- // :TODO: https send |this| to actorUrl
+ static noteFromLocalMessage(webServer, message, cb) {
+ const localUserId = message.getLocalFromUserId();
+ if (!localUserId) {
+ return cb(Errors.UnexpectedState('Invalid user ID for local user!'));
+ }
+
+ async.waterfall(
+ [
+ callback => {
+ return User.getUser(localUserId, callback);
+ },
+ (localUser, callback) => {
+ const remoteActorAccount = message.getRemoteToUser();
+ if (!remoteActorAccount) {
+ return callback(
+ Errors.UnexpectedState(
+ 'Message does not contain a remote address'
+ )
+ );
+ }
+
+ const opts = {};
+ Actor.fromAccountName(
+ remoteActorAccount,
+ opts,
+ (err, remoteActor) => {
+ return callback(err, localUser, remoteActor);
+ }
+ );
+ },
+ (localUser, remoteActor, callback) => {
+ Actor.fromLocalUser(localUser, webServer, (err, localActor) => {
+ return callback(err, localUser, localActor, remoteActor);
+ });
+ },
+ (localUser, localActor, remoteActor, callback) => {
+ // we'll need the entire |activityId| as a linked reference later
+ const activityId = Activity._makeId(webServer, '/create');
+
+ // |remoteActor| is non-null if we fetchd it
+ //const to = message.isPrivate() ? remoteActor ? remoteActor.id : `${ActivityStreamsContext}#Public`;
+
+ // const obj = {
+ // '@context': ActivityStreamsContext,
+ // id: activityId,
+ // type: 'Create',
+ // to: [remoteActor.id],
+ // audience: ['as:Public'],
+ // actor: localActor.id,
+ // object: {
+ // id: Activity._makeId(webServer, '/note'),
+ // type: 'Note',
+ // attributedTo: localActor.id,
+ // to: [remoteActor.id],
+ // audience: ['as:Public'],
+ // content: messageBodyToHtml(message.message.trim()),
+ // },
+ // };
+
+ const obj = {
+ '@context': ActivityStreamsContext,
+ id: activityId,
+ type: 'Create',
+ actor: localActor.id,
+ object: {
+ id: Activity._makeId(webServer, '/note'),
+ type: 'Note',
+ published: getISOTimestampString(message.modTimestamp),
+ attributedTo: localActor.id,
+ // :TODO: inReplyto if this is a reply; we need this store in message meta.
+
+ // :TODO: we may want to turn this to a HTML fragment?
+ content: messageBodyToHtml(message.message.trim()),
+ },
+ };
+
+ // :TODO: this probably needs to change quite a bit based on "groups"
+ // :TODO: verify we need both 'to' fields: https://socialhub.activitypub.rocks/t/problems-posting-to-mastodon-inbox/801/4
+ if (message.isPrivate()) {
+ obj.to = remoteActor.id;
+ obj.object.to = remoteActor.id;
+ } else {
+ const publicInbox = `${ActivityStreamsContext}#Public`;
+ obj.to = publicInbox;
+ obj.object.to = publicInbox;
+ }
+
+ const activity = new Activity(obj);
+ return callback(null, activity, localUser, remoteActor);
+ },
+ ],
+ (err, activity, fromUser, remoteActor) => {
+ return cb(err, { activity, fromUser, remoteActor });
+ }
+ );
+ }
+
+ sendTo(actorUrl, fromUser, webServer, cb) {
+ const privateKey = fromUser.getProperty(UserProps.PrivateKeyMain);
+ if (_.isEmpty(privateKey)) {
+ return cb(
+ Errors.MissingProperty(
+ `User "${fromUser.username}" is missing the '${UserProps.PrivateKeyMain}' property`
+ )
+ );
+ }
+
+ const reqOpts = {
+ headers: {
+ 'Content-Type': 'application/activity+json',
+ },
+ sign: {
+ // :TODO: Make a helper for this
+ key: privateKey,
+ keyId: selfUrl(webServer, fromUser) + '#main-key',
+ authorizationHeaderName: 'Signature',
+ headers: ['(request-target)', 'host', 'date', 'digest', 'content-type'],
+ },
+ };
+
+ const activityJson = JSON.stringify(this);
+ return postJson(actorUrl, activityJson, reqOpts, cb);
+ }
+
+ static _makeId(webServer, prefix = '') {
+ return webServer.buildUrl(`${prefix}/${UUIDv4()}`);
}
};
diff --git a/core/activitypub_actor.js b/core/activitypub_actor.js
index c9dacf85..140268cf 100644
--- a/core/activitypub_actor.js
+++ b/core/activitypub_actor.js
@@ -4,7 +4,6 @@
// ENiGMA½
const actorDb = require('./database.js').dbs.actor;
const { Errors } = require('./enig_error.js');
-const { ActorProps } = require('./activitypub_actor_property');
const UserProps = require('./user_property');
const {
webFingerProfileUrl,
@@ -14,9 +13,10 @@ const {
ActivityStreamsContext,
} = require('./activitypub_util');
const Log = require('./logger').log;
+const { queryWebFinger } = require('./webfinger');
+const EnigAssert = require('./enigma_assert');
// deps
-const assert = require('assert');
const async = require('async');
const _ = require('lodash');
const https = require('https');
@@ -102,6 +102,11 @@ module.exports = class Actor {
owner: userSelfUrl,
publicKeyPem,
};
+
+ EnigAssert(
+ !_.isEmpty(user.getProperty(UserProps.PrivateKeyMain)),
+ 'User has public key but no private key!'
+ );
} else {
Log.warn(
{ username: user.username },
@@ -117,6 +122,8 @@ module.exports = class Actor {
Accept: 'application/activity+json',
};
+ // :TODO: use getJson()
+
https.get(url, { headers }, res => {
if (res.statusCode !== 200) {
return cb(Errors.Invalid(`Bad HTTP status code: ${req.statusCode}`));
@@ -153,6 +160,35 @@ module.exports = class Actor {
});
}
+ static fromAccountName(actorName, options, cb) {
+ // :TODO: cache first -- do we have an Actor for this account already with a OK TTL?
+
+ queryWebFinger(actorName, (err, res) => {
+ if (err) {
+ return cb(err);
+ }
+
+ // we need a link with 'application/activity+json'
+ const links = res.links;
+ if (!Array.isArray(links)) {
+ return cb(Errors.DoesNotExist('No "links" object in WebFinger response'));
+ }
+
+ const activityLink = links.find(l => {
+ return l.type === 'application/activity+json' && l.href?.length > 0;
+ });
+
+ if (!activityLink) {
+ return cb(
+ Errors.DoesNotExist('No Activity link found in WebFinger response')
+ );
+ }
+
+ // we can now query the href value for an Actor
+ return Actor.fromRemoteUrl(activityLink.href, cb);
+ });
+ }
+
static fromJson(json) {
const parsed = JSON.parse(json);
return new Actor(parsed);
diff --git a/core/activitypub_util.js b/core/activitypub_util.js
index 667390c4..3c4632d4 100644
--- a/core/activitypub_util.js
+++ b/core/activitypub_util.js
@@ -20,6 +20,7 @@ exports.selfUrl = selfUrl;
exports.userFromAccount = userFromAccount;
exports.accountFromSelfUrl = accountFromSelfUrl;
exports.getUserProfileTemplatedBody = getUserProfileTemplatedBody;
+exports.messageBodyToHtml = messageBodyToHtml;
// :TODO: more info in default
// this profile template is the *default* for both WebFinger
@@ -160,3 +161,12 @@ function getUserProfileTemplatedBody(
}
);
}
+
+//
+// Apply very basic HTML to a message following
+// Mastodon's supported tags of 'p', 'br', 'a', and 'span':
+// https://blog.joinmastodon.org/2018/06/how-to-implement-a-basic-activitypub-server/
+//
+function messageBodyToHtml(body) {
+ return `
${body.replace(/\r?\n/g, '
')}
`;
+}
diff --git a/core/enig_error.js b/core/enig_error.js
index c9d16a54..40422a94 100644
--- a/core/enig_error.js
+++ b/core/enig_error.js
@@ -54,6 +54,8 @@ exports.Errors = {
HttpError: (reason, reasonCode) =>
new EnigError('HTTP error', -32013, reason, reasonCode),
Timeout: (reason, reasonCode) => new EnigError('Timeout', -32014, reason, reasonCode),
+ MissingProperty: (reason, reasonCode) =>
+ new EnigError('Missing property', -32014, reason, reasonCode),
};
exports.ErrorReasons = {
diff --git a/core/fse.js b/core/fse.js
index c5413989..fd0f6aac 100644
--- a/core/fse.js
+++ b/core/fse.js
@@ -16,7 +16,11 @@ const { MessageAreaConfTempSwitcher } = require('./mod_mixins.js');
const { isAnsi, stripAnsiControlCodes, insert } = require('./string_util.js');
const { stripMciColorCodes, controlCodesToAnsi } = require('./color_codes.js');
const Config = require('./config.js').get;
-const { getAddressedToInfo } = require('./mail_util.js');
+const {
+ getAddressedToInfo,
+ setExternalAddressedToInfo,
+ copyExternalAddressedToInfo,
+} = require('./mail_util.js');
const Events = require('./events.js');
const UserProps = require('./user_property.js');
const SysProps = require('./system_property.js');
@@ -589,15 +593,9 @@ exports.FullScreenEditorModule =
self.replyToMessage &&
self.replyToMessage.isFromRemoteUser()
) {
- self.message.setRemoteToUser(
- self.replyToMessage.meta.System[
- Message.SystemMetaNames.RemoteFromUser
- ]
- );
- self.message.setExternalFlavor(
- self.replyToMessage.meta.System[
- Message.SystemMetaNames.ExternalFlavor
- ]
+ copyExternalAddressedToInfo(
+ self.replyToMessage,
+ self.message
);
return callback(null);
}
@@ -605,21 +603,16 @@ exports.FullScreenEditorModule =
//
// Detect if the user is attempting to send to a remote mail type that we support
//
- // :TODO: how to plug in support without tying to various types here? isSupportedExteranlType() or such
const addressedToInfo = getAddressedToInfo(
self.message.toUserName
);
- if (
- addressedToInfo.name &&
- Message.AddressFlavor.FTN === addressedToInfo.flavor
- ) {
- self.message.setRemoteToUser(addressedToInfo.remote);
- self.message.setExternalFlavor(addressedToInfo.flavor);
- self.message.toUserName = addressedToInfo.name;
+
+ if (setExternalAddressedToInfo(addressedToInfo, self.message)) {
+ // setExternalAddressedToInfo() did what we need
return callback(null);
}
- // we need to look it up
+ // Local user -- we need to look it up
User.getUserIdAndNameByLookup(
self.message.toUserName,
(err, toUserId) => {
diff --git a/core/http_util.js b/core/http_util.js
index 7fd8cf71..e1d0733b 100644
--- a/core/http_util.js
+++ b/core/http_util.js
@@ -1,18 +1,46 @@
const { Errors } = require('./enig_error.js');
// deps
-const { isString, isObject } = require('lodash');
+const { isString, isObject, truncate } = require('lodash');
const https = require('https');
const httpSignature = require('http-signature');
const crypto = require('crypto');
+exports.getJson = getJson;
exports.postJson = postJson;
+function getJson(url, options, cb) {
+ const defaultOptions = {
+ method: 'GET',
+ };
+
+ options = Object.assign({}, defaultOptions, options);
+
+ // :TODO: signing -- DRY this.
+
+ _makeRequest(url, options, (err, body, res) => {
+ if (err) {
+ return cb(err);
+ }
+
+ let parsed;
+ try {
+ parsed = JSON.parse(body);
+ } catch (e) {
+ return cb(e);
+ }
+
+ return cb(null, parsed, res);
+ });
+}
+
function postJson(url, json, options, cb) {
if (!isString(json)) {
json = JSON.stringify(json);
}
+ // :TODO: DRY this method with _makeRequest()
+
const defaultOptions = {
method: 'POST',
body: json,
@@ -32,6 +60,59 @@ function postJson(url, json, options, cb) {
options.headers['Content-Length'] = json.length;
+ const req = https.request(url, options, res => {
+ let body = [];
+ res.on('data', d => {
+ body.push(d);
+ });
+
+ res.on('end', () => {
+ body = Buffer.concat(body).toString();
+
+ if (res.statusCode < 200 || res.statusCode > 299) {
+ return cb(
+ Errors.HttpError(
+ `HTTP error ${res.statusCode}: ${truncate(body, { length: 128 })}`
+ ),
+ body,
+ res
+ );
+ }
+
+ return cb(null, body, res);
+ });
+ });
+
+ if (isObject(options.sign)) {
+ try {
+ httpSignature.sign(req, options.sign);
+ } catch (e) {
+ req.destroy();
+ return cb(Errors.Invalid(`Invalid signing material: ${e}`));
+ }
+ }
+
+ req.on('error', err => {
+ return cb(err);
+ });
+
+ req.on('timeout', () => {
+ req.destroy();
+ return cb(Errors.Timeout('Timeout making HTTP request'));
+ });
+
+ req.write(json);
+ req.end();
+}
+
+function _makeRequest(url, options, cb) {
+ if (options.body && options?.sign?.headers?.includes('digest')) {
+ options.headers['Digest'] = `SHA-256=${crypto
+ .createHash('sha256')
+ .update(options.body)
+ .digest('base64')}`;
+ }
+
const req = https.request(url, options, res => {
let body = [];
res.on('data', d => {
@@ -67,6 +148,5 @@ function postJson(url, json, options, cb) {
return cb(Errors.Timeout('Timeout making HTTP request'));
});
- req.write(json);
req.end();
}
diff --git a/core/mail_util.js b/core/mail_util.js
index 15a467ff..eac3d66d 100644
--- a/core/mail_util.js
+++ b/core/mail_util.js
@@ -1,10 +1,14 @@
/* jslint node: true */
'use strict';
+const EnigmaAssert = require('./enigma_assert.js');
const Address = require('./ftn_address.js');
+const { AddressFlavor } = require('./message.js');
const Message = require('./message.js');
exports.getAddressedToInfo = getAddressedToInfo;
+exports.setExternalAddressedToInfo = setExternalAddressedToInfo;
+exports.copyExternalAddressedToInfo = copyExternalAddressedToInfo;
const EMAIL_REGEX =
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
@@ -31,29 +35,29 @@ function getAddressedToInfo(input) {
if (firstAtPos < 0) {
let addr = Address.fromString(input);
if (Address.isValidAddress(addr)) {
- return { flavor: Message.AddressFlavor.FTN, remote: input };
+ return { flavor: AddressFlavor.FTN, remote: input };
}
const lessThanPos = input.indexOf('<');
if (lessThanPos < 0) {
- return { name: input, flavor: Message.AddressFlavor.Local };
+ return { name: input, flavor: AddressFlavor.Local };
}
const greaterThanPos = input.indexOf('>');
if (greaterThanPos < lessThanPos) {
- return { name: input, flavor: Message.AddressFlavor.Local };
+ return { name: input, flavor: AddressFlavor.Local };
}
addr = Address.fromString(input.slice(lessThanPos + 1, greaterThanPos));
if (Address.isValidAddress(addr)) {
return {
name: input.slice(0, lessThanPos).trim(),
- flavor: Message.AddressFlavor.FTN,
+ flavor: AddressFlavor.FTN,
remote: addr.toString(),
};
}
- return { name: input, flavor: Message.AddressFlavor.Local };
+ return { name: input, flavor: AddressFlavor.Local };
}
if (firstAtPos === 0) {
@@ -63,7 +67,7 @@ function getAddressedToInfo(input) {
if (m) {
return {
name: input.slice(1, secondAtPos),
- flavor: Message.AddressFlavor.ActivityPub,
+ flavor: AddressFlavor.ActivityPub,
remote: input.slice(firstAtPos),
};
}
@@ -78,36 +82,67 @@ function getAddressedToInfo(input) {
if (m) {
return {
name: input.slice(0, lessThanPos).trim(),
- flavor: Message.AddressFlavor.Email,
+ flavor: AddressFlavor.Email,
remote: addr,
};
}
- return { name: input, flavor: Message.AddressFlavor.Local };
+ return { name: input, flavor: AddressFlavor.Local };
}
let m = input.match(EMAIL_REGEX);
if (m) {
return {
name: input.slice(0, firstAtPos),
- flavor: Message.AddressFlavor.Email,
+ flavor: AddressFlavor.Email,
remote: input,
};
}
let addr = Address.fromString(input); // 5D?
if (Address.isValidAddress(addr)) {
- return { flavor: Message.AddressFlavor.FTN, remote: addr.toString() };
+ return { flavor: AddressFlavor.FTN, remote: addr.toString() };
}
addr = Address.fromString(input.slice(firstAtPos + 1).trim());
if (Address.isValidAddress(addr)) {
return {
name: input.slice(0, firstAtPos).trim(),
- flavor: Message.AddressFlavor.FTN,
+ flavor: AddressFlavor.FTN,
remote: addr.toString(),
};
}
- return { name: input, flavor: Message.AddressFlavor.Local };
+ return { name: input, flavor: AddressFlavor.Local };
+}
+
+/// returns true if it's an external address
+function setExternalAddressedToInfo(addressInfo, message) {
+ const isValidAddressInfo = () => {
+ return addressInfo.name.length > 1 && addressInfo.remote.length > 1;
+ };
+
+ switch (addressInfo.flavor) {
+ case AddressFlavor.FTN:
+ case AddressFlavor.Email:
+ case AddressFlavor.QWK:
+ case AddressFlavor.NNTP:
+ case AddressFlavor.ActivityPub:
+ EnigmaAssert(isValidAddressInfo());
+
+ message.setRemoteToUser(addressInfo.remote);
+ message.setExternalFlavor(addressInfo.flavor);
+ message.toUserName = addressInfo.name;
+ return true;
+
+ default:
+ case AddressFlavor.Local:
+ return false;
+ }
+}
+
+function copyExternalAddressedToInfo(fromMessage, toMessage) {
+ const sm = Message.SystemMetaNames;
+ toMessage.setRemoteToUser(fromMessage.meta.System[sm.RemoteFromUser]);
+ toMessage.setExternalFlavor(fromMessage.meta.System[sm.ExternalFlavor]);
}
diff --git a/core/message.js b/core/message.js
index 47c16821..9b300ffe 100644
--- a/core/message.js
+++ b/core/message.js
@@ -101,6 +101,10 @@ const QWKPropertyNames = {
InReplyToNum: 'qwk_in_reply_to_num', // note that we prefer the 'InReplyToMsgId' kludge if available
};
+const ActivityPubPropertyNames = {
+ ActivityId: 'activitypub_activity_id', // Activity ID; FK to AP table entries
+};
+
// :TODO: this is a ugly hack due to bad variable names - clean it up & just _.camelCase(k)!
const MESSAGE_ROW_MAP = {
reply_to_message_id: 'replyToMsgId',
@@ -243,16 +247,29 @@ module.exports = class Message {
this.meta.System[Message.SystemMetaNames.LocalFromUserID] = userId;
}
+ getLocalFromUserId() {
+ let id = _.get(this, 'meta.System.local_from_user_id', 0);
+ return parseInt(id);
+ }
+
setRemoteToUser(remoteTo) {
this.meta.System = this.meta.System || {};
this.meta.System[Message.SystemMetaNames.RemoteToUser] = remoteTo;
}
+ getRemoteToUser() {
+ return _.get(this, 'meta.System.remote_to_user');
+ }
+
setExternalFlavor(flavor) {
this.meta.System = this.meta.System || {};
this.meta.System[Message.SystemMetaNames.ExternalFlavor] = flavor;
}
+ getAddressFlavor() {
+ return _.get(this, 'meta.System.external_flavor', Message.AddressFlavor.Local);
+ }
+
static createMessageUUID(areaTag, modTimestamp, subject, body) {
assert(_.isString(areaTag));
assert(_.isDate(modTimestamp) || moment.isMoment(modTimestamp));
diff --git a/core/scanner_tossers/activitypub.js b/core/scanner_tossers/activitypub.js
new file mode 100644
index 00000000..1ebaa452
--- /dev/null
+++ b/core/scanner_tossers/activitypub.js
@@ -0,0 +1,87 @@
+const Activity = require('../activitypub_activity');
+const Message = require('../message');
+const { MessageScanTossModule } = require('../msg_scan_toss_module');
+const { getServer } = require('../listening_server');
+
+exports.moduleInfo = {
+ name: 'ActivityPub',
+ desc: 'Provides ActivityPub scanner/tosser integration',
+ author: 'NuSkooler',
+};
+
+exports.getModule = class ActivityPubScannerTosser extends MessageScanTossModule {
+ constructor() {
+ super();
+ }
+
+ startup(cb) {
+ return cb(null);
+ }
+
+ shutdown(cb) {
+ return cb(null);
+ }
+
+ record(message) {
+ if (!this._isEnabled()) {
+ return;
+ }
+
+ Activity.noteFromLocalMessage(this._webServer(), message, (err, noteData) => {
+ if (err) {
+ // :TODO: Log me
+ }
+
+ const { activity, fromUser, remoteActor } = noteData;
+
+ // - persist Activity
+ // - sendTo
+ // - update message properties:
+ // * exported
+ // * ActivityPub ID -> activity table
+ activity.sendTo(
+ remoteActor.inbox,
+ fromUser,
+ this._webServer(),
+ (err, respBody, res) => {
+ if (err) {
+ }
+ }
+ );
+ });
+ }
+
+ _isEnabled() {
+ // :TODO: check config to see if AP integration is enabled/etc.
+ return this._webServer();
+ }
+
+ _shouldExportMessage(message) {
+ //
+ // - Private messages: Must be ActivityPub flavor
+ // - Public messages: Must be in area mapped for ActivityPub import/export
+ //
+ // :TODO: Implement the mapping
+ if (
+ Message.AddressFlavor.ActivityPub === message.getAddressFlavor() &&
+ Message.isPrivateAreaTag()
+ ) {
+ return true;
+ }
+
+ return false;
+ }
+
+ _exportToActivityPub(message, cb) {
+ return cb(null);
+ }
+
+ _webServer() {
+ // we have to lazy init
+ if (undefined === this.webServer) {
+ this.webServer = getServer('codes.l33t.enigma.web.server') || null;
+ }
+
+ return this.webServer ? this.webServer.instance : null;
+ }
+};
diff --git a/core/servers/content/web_handlers/activitypub.js b/core/servers/content/web_handlers/activitypub.js
index 45a57b07..6d4d60f1 100644
--- a/core/servers/content/web_handlers/activitypub.js
+++ b/core/servers/content/web_handlers/activitypub.js
@@ -212,55 +212,27 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
// request for the user to review and decide what to do with
// at a later time.
//
+ // :TODO: Implement the queue
const activityPubSettings = ActivityPubSettings.fromUser(user);
if (!activityPubSettings.manuallyApproveFollowers) {
- //
- // :TODO: Implement the queue
Actor.fromLocalUser(user, this.webServer, (err, localActor) => {
if (err) {
- // :TODO:
- return;
+ return this.log.warn(
+ { inbox: actor.inbox, error: err.message },
+ 'Failed to load local Actor for "Accept"'
+ );
}
- // user must have a Private Key
- const privateKey = user.getProperty(UserProps.PrivateKeyMain);
- if (_.isEmpty(privateKey)) {
- // :TODO: Log me
- return;
- }
-
- // :TODO: This stuff should probably be lifted out so it can be called ad-hoc from the queue
const accept = Activity.makeAccept(
this.webServer,
localActor,
activity
);
- const keyId = selfUrl(this.webServer, user) + '#main-key';
-
- const reqOpts = {
- headers: {
- 'Content-Type': 'application/activity+json',
- },
- sign: {
- // :TODO: Make a helper for this
- key: privateKey,
- keyId,
- authorizationHeaderName: 'Signature',
- headers: [
- '(request-target)',
- 'host',
- 'date',
- 'digest',
- 'content-type',
- ],
- },
- };
-
- postJson(
+ accept.sendTo(
actor.inbox,
- JSON.stringify(accept),
- reqOpts,
+ user,
+ this.webServer,
(err, respBody, res) => {
if (err) {
return this.log.warn(
diff --git a/core/system_view_validate.js b/core/system_view_validate.js
index 61c52a52..763e0c47 100644
--- a/core/system_view_validate.js
+++ b/core/system_view_validate.js
@@ -88,10 +88,8 @@ function validateGeneralMailAddressedTo(data, cb) {
// - Local username or real name
// - Supported remote flavors such as FTN, email, ...
//
- // :TODO: remove hard-coded FTN check here. We need a decent way to register global supported flavors with modules.
const addressedToInfo = getAddressedToInfo(data);
-
- if (Message.AddressFlavor.FTN === addressedToInfo.flavor) {
+ if (Message.AddressFlavor.Local !== addressedToInfo.flavor) {
return cb(null);
}
diff --git a/core/webfinger.js b/core/webfinger.js
new file mode 100644
index 00000000..299035a3
--- /dev/null
+++ b/core/webfinger.js
@@ -0,0 +1,49 @@
+const { Errors } = require('./enig_error');
+const { getAddressedToInfo } = require('./mail_util');
+const Message = require('./message');
+const { getJson } = require('./http_util');
+
+// deps
+const https = require('https');
+
+exports.queryWebFinger = queryWebFinger;
+
+function queryWebFinger(account, cb) {
+ // ex: @NuSkooler@toot.community -> https://toot.community/.well-known/webfinger with acct:NuSkooler resource
+ const addrInfo = getAddressedToInfo(account);
+ if (
+ addrInfo.flavor !== Message.AddressFlavor.ActivityPub &&
+ addrInfo.flavor !== Message.AddressFlavor.Email
+ ) {
+ return cb(Errors.Invalid(`Cannot WebFinger "${accountName}"; Missing domain`));
+ }
+
+ const domain = addrInfo.remote.slice(addrInfo.remote.lastIndexOf('@') + 1);
+ if (!domain) {
+ return cb(Errors.Invalid(`Cannot WebFinger "${accountName}"; Missing domain`));
+ }
+
+ const resource = encodeURIComponent(`acct:${account.slice(1)}`); // we need drop the initial '@' prefix
+ const webFingerUrl = `https://${domain}/.well-known/webfinger?resource=${resource}`;
+ getJson(webFingerUrl, {}, (err, json, res) => {
+ if (err) {
+ return cb(err);
+ }
+
+ if (res.statusCode !== 200) {
+ // only accept 200
+ return cb(Errors.DoesNotExist(`Failed to WebFinger URL ${webFingerUrl}`));
+ }
+
+ const contentType = res.headers['content-type'] || '';
+ if (!contentType.startsWith('application/jrd+json')) {
+ return cb(
+ Errors.Invalid(
+ `Invalid Content-Type for WebFinger URL ${webFingerUrl}: ${contentType}`
+ )
+ );
+ }
+
+ return cb(null, json);
+ });
+}