Ability to send/recv public messages in the AP shared inbox areaTag

* Optional subjects
* Resolving followers
* Various cleanup and tidy
This commit is contained in:
Bryan Ashby 2023-02-05 21:10:51 -07:00
parent 36ebda5269
commit 0402de7444
No known key found for this signature in database
GPG Key ID: C2C1B501E4EFD994
9 changed files with 340 additions and 119 deletions

View File

@ -49,12 +49,25 @@ module.exports = class Activity extends ActivityPubObject {
} }
static makeCreate(webServer, actor, obj) { static makeCreate(webServer, actor, obj) {
return new Activity({ const activity = new Activity({
id: Activity.activityObjectId(webServer), id: Activity.activityObjectId(webServer),
to: obj.to,
type: WellKnownActivity.Create, type: WellKnownActivity.Create,
actor, actor,
object: obj, object: obj,
}); });
const copy = n => {
if (obj[n]) {
activity[n] = obj[n];
}
};
copy('to');
copy('cc');
// :TODO: Others?
return activity;
} }
static makeTombstone(obj) { static makeTombstone(obj) {
@ -68,7 +81,7 @@ module.exports = class Activity extends ActivityPubObject {
}); });
} }
sendTo(actorUrl, fromUser, webServer, cb) { sendTo(inboxEndpoint, fromUser, webServer, cb) {
const privateKey = fromUser.getProperty(UserProps.PrivateActivityPubSigningKey); const privateKey = fromUser.getProperty(UserProps.PrivateActivityPubSigningKey);
if (_.isEmpty(privateKey)) { if (_.isEmpty(privateKey)) {
return cb( return cb(
@ -91,7 +104,7 @@ module.exports = class Activity extends ActivityPubObject {
}; };
const activityJson = JSON.stringify(this); const activityJson = JSON.stringify(this);
return postJson(actorUrl, activityJson, reqOpts, cb); return postJson(inboxEndpoint, activityJson, reqOpts, cb);
} }
// :TODO: we need dp/support a bit more here... // :TODO: we need dp/support a bit more here...

View File

@ -12,6 +12,7 @@ const { getJson } = require('../http_util');
// deps // deps
const { isString } = require('lodash'); const { isString } = require('lodash');
const Log = require('../logger');
module.exports = class Collection extends ActivityPubObject { module.exports = class Collection extends ActivityPubObject {
constructor(obj) { constructor(obj) {
@ -260,18 +261,33 @@ module.exports = class Collection extends ActivityPubObject {
return cb(err); return cb(err);
} }
entries = entries || []; try {
entries = (entries || []).map(e => JSON.parse(e.object_json));
} catch (e) {
Log.error(`Collection "${collectionId}" error: ${e.message}`);
}
if (mapper && entries.length > 0) { if (mapper && entries.length > 0) {
entries = entries.map(mapper); entries = entries.map(mapper);
} }
const obj = { let obj;
id: `${collectionId}/page=${page}`, if ('all' === page) {
type: 'OrderedCollectionPage', obj = {
totalItems: entries.length, id: collectionId,
orderedItems: entries, type: 'OrderedCollection',
partOf: collectionId, totalItems: entries.length,
}; orderedItems: entries,
};
} else {
obj = {
id: `${collectionId}/page=${page}`,
type: 'OrderedCollectionPage',
totalItems: entries.length,
orderedItems: entries,
partOf: collectionId,
};
}
return cb(null, new Collection(obj)); return cb(null, new Collection(obj));
} }

View File

@ -50,7 +50,7 @@ module.exports = class Note extends ActivityPubObject {
} }
// A local Message bound for ActivityPub // A local Message bound for ActivityPub
static fromLocalOutgoingMessage(message, webServer, cb) { static fromLocalMessage(message, webServer, cb) {
const localUserId = message.getLocalFromUserId(); const localUserId = message.getLocalFromUserId();
if (!localUserId) { if (!localUserId) {
return cb(Errors.UnexpectedState('Invalid user ID for local user!')); return cb(Errors.UnexpectedState('Invalid user ID for local user!'));
@ -63,7 +63,7 @@ module.exports = class Note extends ActivityPubObject {
} }
const remoteActorAccount = message.getRemoteToUser(); const remoteActorAccount = message.getRemoteToUser();
if (!remoteActorAccount) { if (!remoteActorAccount && message.isPrivate()) {
return cb( return cb(
Errors.UnexpectedState('Message does not contain a remote address') Errors.UnexpectedState('Message does not contain a remote address')
); );
@ -80,9 +80,13 @@ module.exports = class Note extends ActivityPubObject {
}); });
}, },
(fromUser, fromActor, callback) => { (fromUser, fromActor, callback) => {
Actor.fromId(remoteActorAccount, (err, remoteActor) => { if (message.isPrivate()) {
return callback(err, fromUser, fromActor, remoteActor); Actor.fromId(remoteActorAccount, (err, remoteActor) => {
}); return callback(err, fromUser, fromActor, remoteActor);
});
} else {
return callback(null, fromUser, fromActor, null);
}
}, },
(fromUser, fromActor, remoteActor, callback) => { (fromUser, fromActor, remoteActor, callback) => {
if (!message.replyToMsgId) { if (!message.replyToMsgId) {

View File

@ -904,10 +904,20 @@ module.exports = () => {
name: 'System Bulletins', name: 'System Bulletins',
desc: 'Bulletin messages for all users', desc: 'Bulletin messages for all users',
}, },
},
},
activitypub_shared_inbox: { activity_pub: {
name: 'ActivityPub Shared Inbox',
desc: 'Public and shared ActivityPub messages',
areas: {
activitypub_shared: {
name: 'ActivityPub sharedInbox', name: 'ActivityPub sharedInbox',
desc: 'Public shared inbox for ActivityPub', desc: 'Public shared inbox for ActivityPub',
alwaysExportExternal: true,
subjectOptional: true,
addressFlavor: 'activitypub',
}, },
}, },
}, },

View File

@ -174,6 +174,12 @@ exports.FullScreenEditorModule =
) { ) {
// Ignore validation errors if this is the subject field // Ignore validation errors if this is the subject field
// and it's optional // and it's optional
const areaInfo = getMessageAreaByTag(this.messageAreaTag);
if (true === areaInfo.subjectOptional) {
return cb(null, null);
}
// private messages are a little different...
const toView = this.getView('header', MciViewIds.header.to); const toView = this.getView('header', MciViewIds.header.to);
const msgInfo = messageInfoFromAddressedToInfo( const msgInfo = messageInfoFromAddressedToInfo(
getAddressedToInfo(toView.getData()) getAddressedToInfo(toView.getData())
@ -594,7 +600,11 @@ exports.FullScreenEditorModule =
function populateLocalUserInfo(callback) { function populateLocalUserInfo(callback) {
self.message.setLocalFromUserId(self.client.user.userId); self.message.setLocalFromUserId(self.client.user.userId);
if (!self.isPrivateMail()) { const areaInfo = getMessageAreaByTag(self.messageAreaTag);
if (
!self.isPrivateMail() &&
true !== areaInfo.alwaysExportExternal
) {
return callback(null); return callback(null);
} }
@ -636,7 +646,17 @@ exports.FullScreenEditorModule =
self.message.toUserName, self.message.toUserName,
(err, toUserId) => { (err, toUserId) => {
if (err) { if (err) {
return callback(err); if (self.message.isPrivate()) {
return callback(err);
}
if (areaInfo.addressFlavor) {
self.message.setExternalFlavor(
areaInfo.addressFlavor
);
}
return callback(null);
} }
self.message.setLocalToUserId(toUserId); self.message.setLocalToUserId(toUserId);

View File

@ -2,10 +2,13 @@ const WellKnownAreaTags = {
Invalid: '', Invalid: '',
Private: 'private_mail', Private: 'private_mail',
Bulletin: 'local_bulletin', Bulletin: 'local_bulletin',
ActivityPubSharedInbox: 'activitypub_shared_inbox', ActivityPubShared: 'activitypub_shared', // sharedInbox -> HERE -> exported as replies (direct) and outbox items (new posts)
}; };
exports.WellKnownAreaTags = WellKnownAreaTags; exports.WellKnownAreaTags = WellKnownAreaTags;
const WellKnownExternalAreaTags = [WellKnownAreaTags.ActivityPubShared];
exports.WellKnownExternalAreaTags = WellKnownExternalAreaTags;
const WellKnownMetaCategories = { const WellKnownMetaCategories = {
System: 'System', System: 'System',
FtnProperty: 'FtnProperty', FtnProperty: 'FtnProperty',

View File

@ -3,12 +3,18 @@ const Message = require('../message');
const { MessageScanTossModule } = require('../msg_scan_toss_module'); const { MessageScanTossModule } = require('../msg_scan_toss_module');
const { getServer } = require('../listening_server'); const { getServer } = require('../listening_server');
const Log = require('../logger').log; const Log = require('../logger').log;
const { WellKnownAreaTags, AddressFlavor } = require('../message_const');
const { Errors } = require('../enig_error');
const Collection = require('../activitypub/collection');
const Note = require('../activitypub/note');
const { makeUserUrl } = require('../activitypub/util');
const { getAddressedToInfo } = require('../mail_util');
const { PublicCollectionId } = require('../activitypub/const');
const Actor = require('../activitypub/actor');
// deps // deps
const async = require('async'); const async = require('async');
const _ = require('lodash'); const _ = require('lodash');
const Collection = require('../activitypub/collection');
const Note = require('../activitypub/note');
exports.moduleInfo = { exports.moduleInfo = {
name: 'ActivityPub', name: 'ActivityPub',
@ -40,19 +46,83 @@ exports.getModule = class ActivityPubScannerTosser extends MessageScanTossModule
return; return;
} }
//
// Private:
// Send Note directly to another remote Actor's inbox
//
// Public:
// - The original message may be addressed to a non-ActivityPub address
// or something like "All" or "Public"; In this case, ignore that entry
// - Additionally, we need to send to the local Actor's followers via their sharedInbox
//
// To achieve the above for Public, we'll collect the followers from the local
// user, query their unique shared inboxes's, update the Note's addressing,
// then deliver and store.
//
async.waterfall( async.waterfall(
[ [
callback => { callback => {
Note.fromLocalOutgoingMessage( // Private or addressed to a single AP Actor:
Note.fromLocalMessage(message, this._webServer(), (err, noteInfo) => {
return callback(err, noteInfo);
});
},
(noteInfo, callback) => {
if (message.isPrivate()) {
if (!noteInfo.remoteActor) {
return callback(
Errors.UnexpectedState(
'Private messages should contain a remote Actor!'
)
);
}
return callback(null, noteInfo, [noteInfo.remoteActor.id]);
}
// public: we need to build a list of sharedInbox's
this._collectDeliveryEndpoints(
message, message,
this._webServer(), noteInfo.fromUser,
(err, noteInfo) => { (err, deliveryEndpoints) => {
return callback(err, noteInfo); return callback(err, noteInfo, deliveryEndpoints);
} }
); );
}, },
(noteInfo, callback) => { (noteInfo, deliveryEndpoints, callback) => {
const { note, fromUser, remoteActor } = noteInfo; const { note, fromUser } = noteInfo;
//
// Update the Note's addressing:
// - Private:
// to: sharedInboxEndpoints[0]
// - Public:
// to: https://www.w3.org/ns/activitystreams#Public
// ... and the message.getRemoteToUser() value *if*
// the flavor is deemed ActivityPub
// cc: [sharedInboxEndpoints]
//
if (message.isPrivate()) {
note.to = deliveryEndpoints.sharedInboxes[0];
} else {
if (deliveryEndpoints.additionalTo) {
note.to = [
PublicCollectionId,
deliveryEndpoints.additionalTo,
];
} else {
note.to = PublicCollectionId;
}
note.cc = [
deliveryEndpoints.followers,
...deliveryEndpoints.sharedInboxes,
];
if (note.to.length < 2 && note.cc.length < 2) {
// If we only have a generic 'followers' endpoint, there is no where to send to
return callback(null, activity, fromUser);
}
}
const activity = Activity.makeCreate( const activity = Activity.makeCreate(
this._webServer(), this._webServer(),
@ -60,41 +130,59 @@ exports.getModule = class ActivityPubScannerTosser extends MessageScanTossModule
note note
); );
// :TODO: Implement retry logic (connection issues, retryable HTTP status) ?? let allEndpoints = deliveryEndpoints.sharedInboxes;
const inbox = remoteActor.inbox; if (deliveryEndpoints.additionalTo) {
allEndpoints.push(deliveryEndpoints.additionalTo);
}
allEndpoints = Array.from(new Set(allEndpoints)); // unique again
// const inbox = remoteActor.endpoints.sharedInbox; async.eachLimit(
// activity.object.to = 'https://www.w3.org/ns/activitystreams#Public'; allEndpoints,
4,
(inbox, nextInbox) => {
activity.sendTo(
inbox,
fromUser,
this._webServer(),
(err, respBody, res) => {
if (err) {
this.log.warn(
{
inbox,
error: err.message,
},
'Failed to send "Note" Activity to Inbox'
);
} else if (
res.statusCode === 200 ||
res.statusCode === 202
) {
this.log.debug(
{ inbox, uuid: message.uuid },
'Message delivered to Inbox'
);
} else {
this.log.warn(
{
inbox,
statusCode: res.statusCode,
body: _.truncate(respBody, 128),
},
'Unexpected status code'
);
}
activity.sendTo( // If we can't send now, no harm, we'll record to the outbox
inbox, return nextInbox(null);
fromUser, }
this._webServer(), );
(err, respBody, res) => { },
if (err) { () => {
this.log.warn(
{ error: err.message, inbox: remoteActor.inbox },
'Failed to send "Note" Activity to Inbox'
);
} else if (res.statusCode !== 202 && res.statusCode !== 200) {
this.log.warn(
{
inbox: remoteActor.inbox,
statusCode: res.statusCode,
body: _.truncate(respBody, 128),
},
'Unexpected status code'
);
}
// carry on regardless if we sent and record
// the item in the user's Outbox collection
return callback(null, activity, fromUser); return callback(null, activity, fromUser);
} }
); );
}, },
(activity, fromUser, callback) => { (activity, fromUser, callback) => {
// If we failed to send above,
Collection.addOutboxItem( Collection.addOutboxItem(
fromUser, fromUser,
activity, activity,
@ -166,6 +254,90 @@ exports.getModule = class ActivityPubScannerTosser extends MessageScanTossModule
); );
} }
_collectDeliveryEndpoints(message, localUser, cb) {
this._collectFollowersSharedInboxEndpoints(
localUser,
(err, endpoints, followersEndpoint) => {
if (err) {
return cb(err);
}
//
// Don't inspect the remote address/remote to
// Here; We already know this in a public
// area. Instead, see if the user typed in
// a reasonable AP address here. If so, we'll
// try to send directly to them as well.
//
const addrInfo = getAddressedToInfo(message.toUserName);
if (
!message.isPrivate() &&
AddressFlavor.ActivityPub === addrInfo.flavor
) {
Actor.fromId(addrInfo.remote, (err, actor) => {
if (err) {
return cb(err);
}
return cb(null, {
additionalTo: actor.id,
sharedInboxes: endpoints,
followers: followersEndpoint,
});
});
} else {
return cb(null, {
sharedInboxes: endpoints,
followers: followersEndpoint,
});
}
}
);
}
_collectFollowersSharedInboxEndpoints(localUser, cb) {
const localFollowersEndpoint =
makeUserUrl(this._webServer(), localUser, '/ap/users/') + '/followers';
Collection.followers(localFollowersEndpoint, 'all', (err, collection) => {
if (err) {
return cb(err);
}
if (!collection.orderedItems || collection.orderedItems.length < 1) {
// no followers :(
return cb(null, []);
}
async.mapLimit(
collection.orderedItems,
4,
(actorId, nextActorId) => {
Actor.fromId(actorId, (err, actor) => {
return nextActorId(err, actor);
});
},
(err, followerActors) => {
if (err) {
return cb(err);
}
const sharedInboxEndpoints = Array.from(
new Set(
followerActors
.map(actor => {
return _.get(actor, 'endpoints.sharedInbox');
})
.filter(inbox => inbox) // drop nulls
)
);
return cb(null, sharedInboxEndpoints, localFollowersEndpoint);
}
);
});
}
_isEnabled() { _isEnabled() {
// :TODO: check config to see if AP integration is enabled/etc. // :TODO: check config to see if AP integration is enabled/etc.
return this._webServer(); return this._webServer();
@ -183,7 +355,13 @@ exports.getModule = class ActivityPubScannerTosser extends MessageScanTossModule
return true; return true;
} }
// :TODO: Implement the area mapping check for public // Public items do not need a specific 'to'; we'll record to the
// local Actor's outbox and send to any followers we know about
if (message.areaTag === WellKnownAreaTags.ActivityPubShared) {
return true;
}
// :TODO: Implement the area mapping check for public 'groups'
return false; return false;
} }

View File

@ -326,6 +326,14 @@ exports.getModule = class WebServerModule extends ServerModule {
}); });
} }
ok(resp, body = '', headers = { 'Content-Type:': 'text/html' }) {
if (body && !headers['Content-Length']) {
headers['Content-Length'] = Buffer(body).length;
}
resp.writeHead(200, 'OK', body ? headers : null);
return resp.end(body);
}
created(resp, body = '', headers = { 'Content-Type:': 'text/html' }) { created(resp, body = '', headers = { 'Content-Type:': 'text/html' }) {
resp.writeHead(201, 'Created', body ? headers : null); resp.writeHead(201, 'Created', body ? headers : null);
return resp.end(body); return resp.end(body);

View File

@ -189,20 +189,14 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
req.on('end', () => { req.on('end', () => {
const activity = Activity.fromJsonString(Buffer.concat(body).toString()); const activity = Activity.fromJsonString(Buffer.concat(body).toString());
if (!activity) { if (!activity || !activity.isValid()) {
this.log.error( this.log.error(
{ url: req.url, method: req.method, endpoint: 'inbox' }, { url: req.url, method: req.method, endpoint: 'inbox' },
'Failed to parse Activity'
);
return this.webServer.resourceNotFound(resp);
}
if (!activity.isValid()) {
this.log.warn(
{ activity, endpoint: 'inbox' },
'Invalid or unsupported Activity' 'Invalid or unsupported Activity'
); );
return this.webServer.badRequest(resp); return activity
? this.webServer.badRequest(resp)
: this.webServer.notImplemented(resp);
} }
switch (activity.type) { switch (activity.type) {
@ -247,7 +241,7 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
break; break;
} }
return this.webServer.resourceNotFound(resp); return this.webServer.notImplemented(resp);
}); });
} }
@ -259,20 +253,14 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
req.on('end', () => { req.on('end', () => {
const activity = Activity.fromJsonString(Buffer.concat(body).toString()); const activity = Activity.fromJsonString(Buffer.concat(body).toString());
if (!activity) { if (!activity || !activity.isValid()) {
this.log.error( this.log.error(
{ url: req.url, method: req.method, endpoint: 'sharedInbox' }, { url: req.url, method: req.method, endpoint: 'sharedInbox' },
'Failed to parse Activity'
);
return this.webServer.resourceNotFound(resp);
}
if (!activity.isValid()) {
this.log.warn(
{ activity, endpoint: 'sharedInbox' },
'Invalid or unsupported Activity' 'Invalid or unsupported Activity'
); );
return this.webServer.badRequest(resp); return activity
? this.webServer.badRequest(resp)
: this.webServer.notImplemented(resp);
} }
switch (activity.type) { switch (activity.type) {
@ -287,8 +275,11 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
{ type: activity.type }, { type: activity.type },
'Invalid or unknown Activity type' 'Invalid or unknown Activity type'
); );
return this.webServer.resourceNotFound(resp); break;
} }
// don't understand the 'type'
return this.webServer.notImplemented(resp);
}); });
} }
@ -309,28 +300,28 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
} }
inboxUpdateObject(inboxType, req, resp, activity) { inboxUpdateObject(inboxType, req, resp, activity) {
const objectIdToUpdate = _.get(activity, 'object.id'); const updateObjectId = _.get(activity, 'object.id');
const objectType = _.get(activity, 'object.type'); const objectType = _.get(activity, 'object.type');
this.log.info( this.log.info(
{ inboxType, objectId: objectIdToUpdate, type: objectType }, { inboxType, objectId: updateObjectId, type: objectType },
'Inbox Object "Update" request' 'Inbox Object "Update" request'
); );
// :TODO: other types... // :TODO: other types...
if (!objectIdToUpdate || !['Note'].includes(objectType)) { if (!updateObjectId || !['Note'].includes(objectType)) {
return this.webServer.resourceNotFound(resp); return this.webServer.notImplemented(resp);
} }
// Note's are wrapped in Create Activities // Note's are wrapped in Create Activities
Collection.objectByEmbeddedId(objectIdToUpdate, (err, obj) => { Collection.objectByEmbeddedId(updateObjectId, (err, obj) => {
if (err) { if (err) {
return this.webServer.internalServerError(resp, err); return this.webServer.internalServerError(resp, err);
} }
if (!obj) { if (!obj) {
// no match // no match, but respond as accepted and hopefully they don't ask again
return this.webServer.resourceNotFound(resp); return this.webServer.accepted(resp);
} }
// OK, the object exists; Does the caller have permission // OK, the object exists; Does the caller have permission
@ -346,7 +337,7 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
if (updateTargetUrl.host !== updaterUrl.host) { if (updateTargetUrl.host !== updaterUrl.host) {
this.log.warn( this.log.warn(
{ {
objectId: objectIdToUpdate, objectId: updateObjectId,
type: objectType, type: objectType,
updateTargetHost: updateTargetUrl.host, updateTargetHost: updateTargetUrl.host,
requestorHost: updaterUrl.host, requestorHost: updaterUrl.host,
@ -358,7 +349,7 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
Collection.updateCollectionEntry( Collection.updateCollectionEntry(
'inbox', 'inbox',
objectIdToUpdate, updateObjectId,
activity, activity,
err => { err => {
if (err) { if (err) {
@ -367,7 +358,7 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
this.log.info( this.log.info(
{ {
objectId: objectIdToUpdate, objectId: updateObjectId,
type: objectType, type: objectType,
collection: 'inbox', collection: 'inbox',
}, },
@ -444,7 +435,7 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
return this._storeNoteAsMessage( return this._storeNoteAsMessage(
activity.id, activity.id,
'All', 'All',
Message.WellKnownAreaTags.ActivityPubSharedInbox, Message.WellKnownAreaTags.ActivityPubShared,
note, note,
cb cb
); );
@ -527,13 +518,9 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
} }
const body = JSON.stringify(collection); const body = JSON.stringify(collection);
const headers = { return this.webServer.ok(resp, body, {
'Content-Type': ActivityStreamMediaType, 'Content-Type': ActivityStreamMediaType,
'Content-Length': Buffer(body).length, });
};
resp.writeHead(200, headers);
return resp.end(body);
}); });
} }
@ -568,8 +555,7 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
// :TODO: support a template here // :TODO: support a template here
resp.writeHead(200, { 'Content-Type': 'text/html' }); return this.webServer.ok(resp, note.content);
return resp.end(note.content);
}); });
} }
@ -619,11 +605,6 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
'Follow request' 'Follow request'
); );
const ok = () => {
resp.writeHead(200, { 'Content-Type': 'text/html' });
return resp.end('');
};
// //
// If the user blindly accepts Followers, we can persist // If the user blindly accepts Followers, we can persist
// and send an 'Accept' now. Otherwise, we need to queue this // and send an 'Accept' now. Otherwise, we need to queue this
@ -633,7 +614,7 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
const activityPubSettings = ActivityPubSettings.fromUser(localUser); const activityPubSettings = ActivityPubSettings.fromUser(localUser);
if (!activityPubSettings.manuallyApproveFollowers) { if (!activityPubSettings.manuallyApproveFollowers) {
this._recordAcceptedFollowRequest(localUser, remoteActor, activity); this._recordAcceptedFollowRequest(localUser, remoteActor, activity);
return ok(); return this.webServer.ok(resp);
} else { } else {
Collection.addFollowRequest( Collection.addFollowRequest(
localUser, localUser,
@ -645,7 +626,7 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
return this.internalServerError(resp, err); return this.internalServerError(resp, err);
} }
return ok(); return this.webServer.ok(resp);
} }
); );
} }
@ -836,13 +817,7 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
const body = JSON.stringify(localActor); const body = JSON.stringify(localActor);
const headers = { return this.webServer.ok(resp, body, { 'Content-Type': ActivityStreamMediaType });
'Content-Type': ActivityStreamMediaType,
'Content-Length': Buffer(body).length,
};
resp.writeHead(200, headers);
return resp.end(body);
} }
_standardSelfHandler(localUser, localActor, req, resp) { _standardSelfHandler(localUser, localActor, req, resp) {
@ -872,13 +847,7 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
`Serving ActivityPub Profile for "${localUser.username}"` `Serving ActivityPub Profile for "${localUser.username}"`
); );
const headers = { return this.webServer.ok(resp, body, { 'Content-Type': contentType });
'Content-Type': contentType,
'Content-Length': Buffer(body).length,
};
resp.writeHead(200, headers);
return resp.end(body);
} }
); );
} }