Handle Undo
This commit is contained in:
parent
d9e4b66a35
commit
ce7dd8e1cd
|
@ -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');
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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}`);
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue