Return a outbox WIP
This commit is contained in:
parent
157b90687c
commit
5e5c9236ec
|
@ -1,18 +1,21 @@
|
|||
const { isString, isObject } = require('lodash');
|
||||
const { v4: UUIDv4 } = require('uuid');
|
||||
const {
|
||||
ActivityStreamsContext,
|
||||
messageBodyToHtml,
|
||||
selfUrl,
|
||||
} = require('../activitypub/util');
|
||||
const { Errors } = require('../enig_error');
|
||||
makeUserUrl,
|
||||
} = require('./util');
|
||||
const User = require('../user');
|
||||
const Actor = require('../activitypub/actor');
|
||||
const Actor = require('./actor');
|
||||
const { Errors } = require('../enig_error');
|
||||
const { getISOTimestampString } = require('../database');
|
||||
const UserProps = require('../user_property');
|
||||
const { postJson } = require('../http_util');
|
||||
const { getOutboxEntries } = require('./db');
|
||||
const { WellKnownLocations } = require('../servers/content/web');
|
||||
|
||||
// deps
|
||||
const { isString, isObject } = require('lodash');
|
||||
const { v4: UUIDv4 } = require('uuid');
|
||||
const async = require('async');
|
||||
const _ = require('lodash');
|
||||
|
||||
|
@ -119,6 +122,7 @@ module.exports = class Activity {
|
|||
type: 'Note',
|
||||
published: getISOTimestampString(message.modTimestamp),
|
||||
attributedTo: localActor.id,
|
||||
audience: [message.isPrivate() ? 'as:Private' : 'as:Public'],
|
||||
// :TODO: inReplyto if this is a reply; we need this store in message meta.
|
||||
|
||||
content: messageBodyToHtml(message.message.trim()),
|
||||
|
@ -146,6 +150,37 @@ module.exports = class Activity {
|
|||
);
|
||||
}
|
||||
|
||||
static fromOutboxEntries(owningUser, webServer, cb) {
|
||||
// :TODO: support paging
|
||||
const getOpts = {
|
||||
create: true, // items marked 'Create'
|
||||
};
|
||||
getOutboxEntries(owningUser, getOpts, (err, entries) => {
|
||||
if (err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
const obj = {
|
||||
'@context': ActivityStreamsContext,
|
||||
// :TODO: makeOutboxUrl() and use elsewhere also
|
||||
id: makeUserUrl(webServer, owningUser, '/ap/users') + '/outbox',
|
||||
type: 'OrderedCollection',
|
||||
totalItems: entries.length,
|
||||
orderedItems: entries.map(e => {
|
||||
return {
|
||||
'@context': ActivityStreamsContext,
|
||||
id: e.activity.id,
|
||||
type: 'Create',
|
||||
actor: e.activity.actor,
|
||||
object: e.activity.object,
|
||||
};
|
||||
}),
|
||||
};
|
||||
|
||||
return cb(null, new Activity(obj));
|
||||
});
|
||||
}
|
||||
|
||||
sendTo(actorUrl, fromUser, webServer, cb) {
|
||||
const privateKey = fromUser.getProperty(UserProps.PrivateKeyMain);
|
||||
if (_.isEmpty(privateKey)) {
|
||||
|
@ -173,7 +208,10 @@ module.exports = class Activity {
|
|||
return postJson(actorUrl, activityJson, reqOpts, cb);
|
||||
}
|
||||
|
||||
static _makeFullId(webServer, prefix, uuid = '') {
|
||||
return webServer.buildUrl(`/${prefix}/${uuid || UUIDv4()}`);
|
||||
static _makeFullId(webServer, prefix) {
|
||||
// e.g. http://some.host/_enig/ap/note/bf81a22e-cb3e-41c8-b114-21f375b61124
|
||||
return webServer.buildUrl(
|
||||
WellKnownLocations.Internal + `/ap/${prefix}/${UUIDv4()}`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,17 +1,57 @@
|
|||
const apDb = require('../database').dbs.activitypub;
|
||||
|
||||
exports.persistToOutbox = persistToOutbox;
|
||||
exports.getOutboxEntries = getOutboxEntries;
|
||||
|
||||
function persistToOutbox(activity, userId, messageId, cb) {
|
||||
function persistToOutbox(activity, fromUser, message, cb) {
|
||||
const activityJson = JSON.stringify(activity);
|
||||
|
||||
apDb.run(
|
||||
`INSERT INTO activitypub_outbox (activity_id, user_id, message_id, activity_json)
|
||||
VALUES (?, ?, ?, ?);`,
|
||||
[activity.id, userId, messageId, activityJson],
|
||||
`INSERT INTO outbox (activity_id, user_id, message_id, activity_json, published_timestamp)
|
||||
VALUES (?, ?, ?, ?, ?);`,
|
||||
[
|
||||
activity.id,
|
||||
fromUser.userId,
|
||||
message.messageId,
|
||||
activityJson,
|
||||
activity.object.published,
|
||||
],
|
||||
function res(err) {
|
||||
// non-arrow for 'this' scope
|
||||
return cb(err, this.lastID);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function getOutboxEntries(owningUser, options, cb) {
|
||||
apDb.all(
|
||||
`SELECT id, activity_id, message_id, activity_json, published_timestamp
|
||||
FROM outbox
|
||||
WHERE user_id = ? AND json_extract(activity_json, '$.type') = "Create";`,
|
||||
[owningUser.userId],
|
||||
(err, rows) => {
|
||||
if (err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
const entries = rows.map(r => {
|
||||
let parsed;
|
||||
try {
|
||||
parsed = JSON.parse(r.activity_json);
|
||||
} catch (e) {
|
||||
return cb(e);
|
||||
}
|
||||
|
||||
return {
|
||||
id: r.id,
|
||||
activityId: r.activity_id,
|
||||
messageId: r.message_id,
|
||||
activity: parsed,
|
||||
published: r.published_timestamp,
|
||||
};
|
||||
});
|
||||
|
||||
return cb(null, entries);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
const UserProps = require('./user_property');
|
||||
const UserProps = require('../user_property');
|
||||
|
||||
module.exports = class ActivityPubSettings {
|
||||
constructor(obj) {
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
const { WellKnownLocations } = require('./servers/content/web');
|
||||
const User = require('./user');
|
||||
const { Errors, ErrorReasons } = require('./enig_error');
|
||||
const UserProps = require('./user_property');
|
||||
const ActivityPubSettings = require('./activitypub/settings');
|
||||
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');
|
||||
|
||||
// deps
|
||||
const _ = require('lodash');
|
||||
|
|
|
@ -502,26 +502,33 @@ dbs.message.run(
|
|||
return cb(null);
|
||||
},
|
||||
activitypub: cb => {
|
||||
// private INTEGER NOT NULL, -- Is this Activity private?
|
||||
dbs.activitypub.run(
|
||||
`CREATE TABLE IF NOT EXISTS activitypub_outbox (
|
||||
id INTEGER PRIMARY KEY, -- Local ID
|
||||
activity_id VARCHAR NOT NULL, -- Fully qualified Activity ID/URL
|
||||
user_id INTEGER NOT NULL, -- Local user ID
|
||||
message_id INTEGER NOT NULL, -- Local message ID
|
||||
activity_json VARCHAR NOT NULL, -- Activity in JSON format
|
||||
`CREATE TABLE IF NOT EXISTS outbox (
|
||||
id INTEGER PRIMARY KEY, -- Local ID
|
||||
activity_id VARCHAR NOT NULL, -- Fully qualified Activity ID/URL (activity.id)
|
||||
user_id INTEGER NOT NULL, -- Local user ID
|
||||
message_id INTEGER NOT NULL, -- Local message ID
|
||||
activity_json VARCHAR NOT NULL, -- Activity in JSON format
|
||||
published_timestamp DATETIME NOT NULL, -- (activity.object.published))
|
||||
|
||||
UNIQUE(message_id, activity_id)
|
||||
);`
|
||||
);
|
||||
|
||||
dbs.activitypub.run(
|
||||
`CREATE INDEX IF NOT EXISTS activitypub_outbox_user_id_index0
|
||||
ON activitypub_outbox (user_id);`
|
||||
`CREATE INDEX IF NOT EXISTS outbox_user_id_index0
|
||||
ON outbox (user_id);`
|
||||
);
|
||||
|
||||
dbs.activitypub.run(
|
||||
`CREATE INDEX IF NOT EXISTS activitypub_outbox_activity_id_index0
|
||||
ON activitypub_outbox (activity_id);`
|
||||
`CREATE INDEX IF NOT EXISTS outbox_activity_id_index0
|
||||
ON outbox (activity_id);`
|
||||
);
|
||||
|
||||
dbs.activitypub.run(
|
||||
`CREATE INDEX IF NOT EXISTS outbox_activity_json_type_index0
|
||||
ON outbox (json_extract(activity_json, '$.type'));`
|
||||
);
|
||||
|
||||
return cb(null);
|
||||
|
|
|
@ -82,20 +82,15 @@ exports.getModule = class ActivityPubScannerTosser extends MessageScanTossModule
|
|||
);
|
||||
},
|
||||
(activity, fromUser, callback) => {
|
||||
persistToOutbox(
|
||||
activity,
|
||||
fromUser.userId,
|
||||
message.messageId,
|
||||
(err, localId) => {
|
||||
if (!err) {
|
||||
this.log.debug(
|
||||
{ localId, activityId: activity.id },
|
||||
'Note Activity persisted to database'
|
||||
);
|
||||
}
|
||||
return callback(err, activity);
|
||||
persistToOutbox(activity, fromUser, message, (err, localId) => {
|
||||
if (!err) {
|
||||
this.log.debug(
|
||||
{ localId, activityId: activity.id },
|
||||
'Note Activity persisted to database'
|
||||
);
|
||||
}
|
||||
);
|
||||
return callback(err, activity);
|
||||
});
|
||||
},
|
||||
(activity, callback) => {
|
||||
// mark exported
|
||||
|
|
|
@ -1,6 +1,3 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
// ENiGMA½
|
||||
const Log = require('../../logger.js').log;
|
||||
const ServerModule = require('../../server_module.js').ServerModule;
|
||||
|
|
|
@ -4,11 +4,14 @@ const {
|
|||
getUserProfileTemplatedBody,
|
||||
DefaultProfileTemplate,
|
||||
accountFromSelfUrl,
|
||||
ActivityStreamsContext,
|
||||
makeUserUrl,
|
||||
} = require('../../../activitypub/util');
|
||||
const Config = require('../../../config').get;
|
||||
const Activity = require('../../../activitypub/activity');
|
||||
const ActivityPubSettings = require('../../../activitypub/settings');
|
||||
const Actor = require('../../../activitypub/actor');
|
||||
const { getOutboxEntries } = require('../../../activitypub/db');
|
||||
|
||||
// deps
|
||||
const _ = require('lodash');
|
||||
|
@ -22,6 +25,8 @@ exports.moduleInfo = {
|
|||
packageName: 'codes.l33t.enigma.web.handler.activitypub',
|
||||
};
|
||||
|
||||
const ActivityJsonMime = 'application/activity+json';
|
||||
|
||||
exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
|
||||
constructor() {
|
||||
super();
|
||||
|
@ -45,6 +50,12 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
|
|||
handler: this._inboxPostHandler.bind(this),
|
||||
});
|
||||
|
||||
this.webServer.addRoute({
|
||||
method: 'GET',
|
||||
path: /^\/_enig\/ap\/users\/.+\/outbox(\?page=true)?$/,
|
||||
handler: this._outboxGetHandler.bind(this),
|
||||
});
|
||||
|
||||
// :TODO: NYI
|
||||
// this.webServer.addRoute({
|
||||
// method: 'GET',
|
||||
|
@ -80,7 +91,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 = [
|
||||
'application/activity+json',
|
||||
ActivityJsonMime,
|
||||
'application/ld+json',
|
||||
'application/json',
|
||||
];
|
||||
|
@ -96,26 +107,20 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
|
|||
|
||||
_inboxPostHandler(req, resp) {
|
||||
// the request must be signed, and the signature must be valid
|
||||
const signature = this._parseSignature(req);
|
||||
const signature = this._parseAndValidateSignature(req);
|
||||
if (!signature) {
|
||||
return this.webServer.resourceNotFound(resp);
|
||||
return this.webServer.accessDenied(resp);
|
||||
}
|
||||
|
||||
// quick check up front
|
||||
const keyId = signature.keyId;
|
||||
if (!this._validateKeyId(keyId)) {
|
||||
return this.webServer.resourceNotFound(resp);
|
||||
}
|
||||
|
||||
let body = '';
|
||||
req.on('data', data => {
|
||||
body += data;
|
||||
const body = [];
|
||||
req.on('data', d => {
|
||||
body.push(d);
|
||||
});
|
||||
|
||||
req.on('end', () => {
|
||||
let activity;
|
||||
try {
|
||||
activity = Activity.fromJson(body);
|
||||
activity = Activity.fromJson(Buffer.concat(body).toString());
|
||||
} catch (e) {
|
||||
this.log.error(
|
||||
{ error: e.message, url: req.url, method: req.method },
|
||||
|
@ -125,7 +130,7 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
|
|||
}
|
||||
|
||||
if (!activity.isValid()) {
|
||||
// :TODO: Log me
|
||||
this.log.warn({ activity }, 'Invalid or unsupported Activity');
|
||||
return this.webServer.webServer.badRequest(resp);
|
||||
}
|
||||
|
||||
|
@ -148,19 +153,110 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
|
|||
});
|
||||
}
|
||||
|
||||
_parseSignature(req) {
|
||||
// https://docs.gotosocial.org/en/latest/federation/behaviors/outbox/
|
||||
_outboxGetHandler(req, resp) {
|
||||
this.log.trace({ url: req.url }, 'Request for "outbox"');
|
||||
|
||||
// the request must be signed, and the signature must be valid
|
||||
const signature = this._parseAndValidateSignature(req);
|
||||
if (!signature) {
|
||||
return this.webServer.accessDenied(resp);
|
||||
}
|
||||
|
||||
// /_enig/ap/users/SomeName/outbox -> SomeName
|
||||
const url = new URL(req.url, `https://${req.headers.host}`);
|
||||
const m = url.pathname.match(/^\/_enig\/ap\/users\/(.+)\/outbox$/);
|
||||
if (!m || !m[1]) {
|
||||
return this.webServer.resourceNotFound(resp);
|
||||
}
|
||||
|
||||
const accountName = m[1];
|
||||
userFromAccount(accountName, (err, user) => {
|
||||
if (err) {
|
||||
this.log.info(
|
||||
{ reason: err.message, accountName: accountName },
|
||||
`No user "${accountName}" for "self"`
|
||||
);
|
||||
return this.webServer.resourceNotFound(resp);
|
||||
}
|
||||
|
||||
// // we return a OrderedCollection response if this request
|
||||
// // is not explicitly for a page of the collection
|
||||
// const wantPage = url.searchParams.get('page') === 'true';
|
||||
// if (!wantPage) {
|
||||
// const outboxUrl = makeUserUrl(this.webServer, user, '/ap/users/') + '/outbox';
|
||||
// const body = JSON.stringify({
|
||||
// '@context': ActivityStreamsContext,
|
||||
// id: outboxUrl,
|
||||
// type: 'OrderedCollection',
|
||||
// first: `${outboxUrl}?page=true`,
|
||||
// });
|
||||
|
||||
// const headers = {
|
||||
// 'Content-Type': 'application/activity+json',
|
||||
// 'Content-Length': body.length,
|
||||
// };
|
||||
|
||||
// resp.writeHead(200, headers);
|
||||
// return resp.end(body);
|
||||
// }
|
||||
|
||||
Activity.fromOutboxEntries(user, this.webServer, (err, activity) => {
|
||||
if (err) {
|
||||
// :TODO: LOG ME
|
||||
return this.webServer.internalServerError(resp);
|
||||
}
|
||||
|
||||
const body = JSON.stringify(activity);
|
||||
const headers = {
|
||||
'Content-Type': ActivityJsonMime,
|
||||
'Content-Length': body.length,
|
||||
};
|
||||
|
||||
resp.writeHead(200, headers);
|
||||
return resp.end(body);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_parseAndValidateSignature(req) {
|
||||
let signature;
|
||||
try {
|
||||
// :TODO: validate options passed to parseRequest()
|
||||
return httpSignature.parseRequest(req);
|
||||
signature = httpSignature.parseRequest(req);
|
||||
} catch (e) {
|
||||
this.log.warn(
|
||||
{ error: e.message, url: req.url, method: req.method },
|
||||
'Failed to parse HTTP signature'
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
// quick check up front
|
||||
const keyId = signature.keyId;
|
||||
if (!this._validateKeyId(keyId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return signature;
|
||||
}
|
||||
|
||||
_validateKeyId(keyId) {
|
||||
if (!keyId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// we only accept main-key currently
|
||||
return keyId.endsWith('#main-key');
|
||||
}
|
||||
|
||||
_inboxFollowRequestHandler(signature, activity, req, resp) {
|
||||
this.log.trace(
|
||||
{ actor: activity.actor },
|
||||
`Follow request from ${activity.actor}`
|
||||
);
|
||||
|
||||
// :TODO: trace
|
||||
const accountName = accountFromSelfUrl(activity.object);
|
||||
if (!accountName) {
|
||||
return this.webServer.badRequest(resp);
|
||||
|
@ -266,15 +362,6 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
|
|||
});
|
||||
}
|
||||
|
||||
_validateKeyId(keyId) {
|
||||
if (!keyId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// we only accept main-key currently
|
||||
return keyId.endsWith('#main-key');
|
||||
}
|
||||
|
||||
_authorizeInteractionHandler(req, resp) {
|
||||
console.log(req);
|
||||
}
|
||||
|
@ -294,7 +381,7 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
|
|||
const body = JSON.stringify(actor);
|
||||
|
||||
const headers = {
|
||||
'Content-Type': 'application/activity+json',
|
||||
'Content-Type': ActivityJsonMime,
|
||||
'Content-Length': body.length,
|
||||
};
|
||||
|
||||
|
|
Loading…
Reference in New Issue