Handle Undo

This commit is contained in:
Bryan Ashby 2023-01-21 01:19:19 -07:00
parent d9e4b66a35
commit ce7dd8e1cd
No known key found for this signature in database
GPG Key ID: C2C1B501E4EFD994
6 changed files with 129 additions and 74 deletions

View File

@ -24,11 +24,6 @@ module.exports = class Activity extends ActivityPubObject {
return WellKnownActivityTypes; return WellKnownActivityTypes;
} }
static fromJsonString(json) {
const parsed = JSON.parse(json);
return new Activity(parsed);
}
// https://www.w3.org/TR/activitypub/#accept-activity-inbox // https://www.w3.org/TR/activitypub/#accept-activity-inbox
static makeAccept(webServer, localActor, followRequest, id = null) { static makeAccept(webServer, localActor, followRequest, id = null) {
id = id || Activity._makeFullId(webServer, 'accept'); id = id || Activity._makeFullId(webServer, 'accept');

View File

@ -15,45 +15,40 @@ const Log = require('../logger').log;
const { queryWebFinger } = require('../webfinger'); const { queryWebFinger } = require('../webfinger');
const EnigAssert = require('../enigma_assert'); const EnigAssert = require('../enigma_assert');
const ActivityPubSettings = require('./settings'); const ActivityPubSettings = require('./settings');
const ActivityPubObject = require('./object');
// deps // deps
const _ = require('lodash'); const _ = require('lodash');
const isString = require('lodash/isString');
const mimeTypes = require('mime-types'); const mimeTypes = require('mime-types');
const { getJson } = require('../http_util.js'); const { getJson } = require('../http_util.js');
// https://www.w3.org/TR/activitypub/#actor-objects // https://www.w3.org/TR/activitypub/#actor-objects
module.exports = class Actor { module.exports = class Actor extends ActivityPubObject {
constructor(obj) { constructor(obj) {
this['@context'] = [ActivityStreamsContext]; super(obj);
if (obj) {
Object.assign(this, obj);
} else {
this.id = '';
this.type = '';
this.inbox = '';
this.outbox = '';
this.following = '';
this.followers = '';
}
} }
isValid() { isValid() {
if (!super.isValid()) {
return false;
}
if ( if (
!Array.isArray(this['@context']) || !['Person', 'Group', 'Organization', 'Service', 'Application'].includes(
this['@context'][0] !== ActivityStreamsContext this.type
)
) { ) {
return false; return false;
} }
if (!isString(this.type) || this.type.length < 1) { const linksValid = ['inbox', 'outbox', 'following', 'followers'].every(l => {
// must be valid if set
if (this[l] && !isValidLink(this[l])) {
return false; return false;
} }
return true;
const linksValid = ['inbox', 'outbox', 'following', 'followers'].every(p => {
return isValidLink(this[p]);
}); });
if (!linksValid) { if (!linksValid) {
return false; return false;
} }
@ -136,11 +131,11 @@ module.exports = class Actor {
} }
static fromRemoteUrl(url, cb) { static fromRemoteUrl(url, cb) {
// :TODO: cache first
const headers = { const headers = {
Accept: 'application/activity+json', Accept: 'application/activity+json',
}; };
// :TODO: use getJson()
getJson(url, { headers }, (err, actor) => { getJson(url, { headers }, (err, actor) => {
if (err) { if (err) {
return cb(err); return cb(err);

View File

@ -35,7 +35,13 @@ module.exports = class Collection extends ActivityPubObject {
} }
static addFollower(owningUser, followingActor, cb) { static addFollower(owningUser, followingActor, cb) {
return Collection.addToCollection('followers', owningUser, followingActor, cb); return Collection.addToCollection(
'followers',
owningUser,
followingActor.id,
followingActor,
cb
);
} }
static getOrdered(name, owningUser, includePrivate, page, mapper, webServer, cb) { static getOrdered(name, owningUser, includePrivate, page, mapper, webServer, cb) {
@ -45,7 +51,7 @@ module.exports = class Collection extends ActivityPubObject {
if (!page) { if (!page) {
return apDb.get( return apDb.get(
`SELECT COUNT(id) AS count `SELECT COUNT(id) AS count
FROM collection_entry FROM collection
WHERE name = ?;`, WHERE name = ?;`,
[name], [name],
(err, row) => { (err, row) => {
@ -77,8 +83,8 @@ module.exports = class Collection extends ActivityPubObject {
// :TODO: actual paging... // :TODO: actual paging...
apDb.all( apDb.all(
`SELECT entry_json `SELECT obj_json
FROM collection_entry FROM collection
WHERE user_id = ? AND name = ? WHERE user_id = ? AND name = ?
ORDER BY timestamp;`, ORDER BY timestamp;`,
[owningUser.userId, name], [owningUser.userId, name],
@ -105,15 +111,15 @@ module.exports = class Collection extends ActivityPubObject {
); );
} }
static addToCollection(name, owningUser, entry, cb) { static addToCollection(name, owningUser, objectId, obj, cb) {
if (!isString(entry)) { if (!isString(obj)) {
entry = JSON.stringify(entry); obj = JSON.stringify(obj);
} }
apDb.run( apDb.run(
`INSERT OR IGNORE INTO collection_entry (name, timestamp, user_id, entry_json) `INSERT OR IGNORE INTO collection (name, timestamp, user_id, obj_id, obj_json)
VALUES (?, ?, ?, ?);`, VALUES (?, ?, ?, ?, ?);`,
[name, getISOTimestampString(), owningUser.userId, entry], [name, getISOTimestampString(), owningUser.userId, objectId, obj],
function res(err) { function res(err) {
// non-arrow for 'this' scope // non-arrow for 'this' scope
if (err) { if (err) {
@ -123,4 +129,15 @@ module.exports = class Collection extends ActivityPubObject {
} }
); );
} }
static remoteFromCollectionById(name, owningUser, objectId, cb) {
apDb.run(
`DELETE FROM collection
WHERE user_id = ? AND name = ? AND obj_id = ?;`,
[owningUser.userId, name, objectId],
err => {
return cb(err);
}
);
}
}; };

View File

@ -532,23 +532,21 @@ dbs.message.run(
); );
dbs.activitypub.run( dbs.activitypub.run(
`CREATE TABLE IF NOT EXISTS collection_entry ( `CREATE TABLE IF NOT EXISTS collection (
id INTEGER PRIMARY KEY, -- Auto-generated key id INTEGER PRIMARY KEY, -- Auto-generated key
name VARCHAR NOT NULL, -- examples: followers, follows, ... name VARCHAR NOT NULL, -- examples: followers, follows, ...
timestamp DATETIME NOT NULL, -- Timestamp in which this entry was created timestamp DATETIME NOT NULL, -- Timestamp in which this entry was created
user_id INTEGER NOT NULL, -- Local, owning user ID user_id INTEGER NOT NULL, -- Local, owning user ID
entry_json VARCHAR NOT NULL -- Varies by collection obj_id VARCHAR NOT NULL, -- Object ID from obj_json.id
obj_json VARCHAR NOT NULL, -- Object varies by collection (obj_json.type)
UNIQUE(name, user_id, obj_id)
);` );`
); );
dbs.activitypub.run( dbs.activitypub.run(
`CREATE INDEX IF NOT EXISTS collection_entry_by_user_index0 `CREATE INDEX IF NOT EXISTS collection_entry_by_user_index0
ON collection_entry (name, user_id);` ON collection (name, user_id);`
);
dbs.activitypub.run(
`CREATE UNIQUE INDEX IF NOT EXISTS collection_entry_unique_index0
ON collection_entry (name, user_id, json_extract(entry_json, '$.id'));`
); );
return cb(null); return cb(null);

View File

@ -346,6 +346,10 @@ exports.getModule = class WebServerModule extends ServerModule {
); );
} }
notImplemented(resp) {
return this.respondWithError(resp, 501, 'Not implemented.', 'Not Implemented');
}
tryRouteIndex(req, resp, cb) { tryRouteIndex(req, resp, cb) {
const tryFiles = Config().contentServers.web.tryFiles || [ const tryFiles = Config().contentServers.web.tryFiles || [
'index.html', 'index.html',

View File

@ -10,6 +10,7 @@ const Activity = require('../../../activitypub/activity');
const ActivityPubSettings = require('../../../activitypub/settings'); const ActivityPubSettings = require('../../../activitypub/settings');
const Actor = require('../../../activitypub/actor'); const Actor = require('../../../activitypub/actor');
const Collection = require('../../../activitypub/collection'); const Collection = require('../../../activitypub/collection');
const EnigAssert = require('../../../enigma_assert');
// deps // deps
const _ = require('lodash'); const _ = require('lodash');
@ -46,7 +47,13 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
this.webServer.addRoute({ this.webServer.addRoute({
method: 'POST', method: 'POST',
path: /^\/_enig\/ap\/users\/.+\/inbox$/, path: /^\/_enig\/ap\/users\/.+\/inbox$/,
handler: this._inboxPostHandler.bind(this), handler: (req, resp) => {
return this._enforceSigningPolicy(
req,
resp,
this._inboxPostHandler.bind(this)
);
},
}); });
this.webServer.addRoute({ this.webServer.addRoute({
@ -102,7 +109,7 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
return this.webServer.accessDenied(resp); return this.webServer.accessDenied(resp);
} }
return next(req, resp); return next(req, resp, signature);
} }
_selfUrlRequestHandler(req, resp) { _selfUrlRequestHandler(req, resp) {
@ -146,12 +153,8 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
}); });
} }
_inboxPostHandler(req, resp) { _inboxPostHandler(req, resp, signature) {
// the request must be signed, and the signature must be valid EnigAssert(signature, 'Called without signature!');
const signature = this._parseAndValidateSignature(req);
if (!signature) {
return this.webServer.accessDenied(resp);
}
const body = []; const body = [];
req.on('data', d => { req.on('data', d => {
@ -161,7 +164,8 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
req.on('end', () => { req.on('end', () => {
let activity; let activity;
try { try {
activity = Activity.fromJsonString(Buffer.concat(body).toString()); activity = JSON.parse(Buffer.concat(body).toString());
activity = new Activity(activity);
} catch (e) { } catch (e) {
this.log.error( this.log.error(
{ error: e.message, url: req.url, method: req.method }, { error: e.message, url: req.url, method: req.method },
@ -175,26 +179,28 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
return this.webServer.badRequest(resp); return this.webServer.badRequest(resp);
} }
const activityFunctions = { switch (activity.type) {
Follow: this._inboxFollowRequestHandler.bind(this), case 'Follow':
// TODO: 'Create', 'Update', etc.
};
if (_.has(activityFunctions, activity.type)) {
return this._withUserRequestHandler( return this._withUserRequestHandler(
signature, signature,
activity, activity,
activityFunctions[activity.type], this._inboxFollowRequestHandler.bind(this),
req, req,
resp resp
); );
} else {
this.log.debug( case 'Undo':
return this._inboxUndoRequestHandler(activity, req, resp);
default:
this.log.warn(
{ type: activity.type }, { type: activity.type },
`Unsupported Activity type "${activity.type}"` `Unsupported Activity type "${activity.type}"`
); );
return this.webServer.resourceNotFound(resp); break;
} }
return this.webServer.resourceNotFound(resp);
}); });
} }
@ -332,7 +338,7 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
} }
_inboxFollowRequestHandler(activity, remoteActor, user, resp) { _inboxFollowRequestHandler(activity, remoteActor, user, resp) {
this.log.debug({ user_id: user.userId, actor: activity.actor }, 'Follow request'); this.log.info({ user_id: user.userId, actor: activity.actor }, 'Follow request');
// //
// If the user blindly accepts Followers, we can persist // If the user blindly accepts Followers, we can persist
@ -351,6 +357,46 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
return resp.end(''); return resp.end('');
} }
_inboxUndoRequestHandler(activity, req, resp) {
this.log.info({ actor: activity.actor }, 'Undo request');
const url = new URL(req.url, `https://${req.headers.host}`);
const accountName = this._accountNameFromUserPath(url, 'inbox');
if (!accountName) {
return this.webServer.resourceNotFound(resp);
}
userFromAccount(accountName, (err, user) => {
if (err) {
return this.webServer.resourceNotFound(resp);
}
// we only understand Follow right now
if (!activity.object || activity.object.type !== 'Follow') {
return this.webServer.notImplemented(resp);
}
Collection.remoteFromCollectionById(
'followers',
user,
activity.actor,
err => {
if (err) {
return this.webServer.internalServerError(resp, err);
}
this.log.info(
{ userId: user.userId, actor: activity.actor },
'Undo "Follow" (un-follow) success'
);
resp.writeHead(202);
return resp.end('');
}
);
});
}
_withUserRequestHandler(signature, activity, activityHandler, req, resp) { _withUserRequestHandler(signature, activity, activityHandler, req, resp) {
this.log.debug({ actor: activity.actor }, `Inbox request from ${activity.actor}`); this.log.debug({ actor: activity.actor }, `Inbox request from ${activity.actor}`);