mirror of
https://github.com/NodeBB/NodeBB.git
synced 2025-11-01 03:26:04 +01:00
refactor: activitypub sending to handle signed requests from categories
This commit is contained in:
@@ -30,13 +30,13 @@ Actors.assert = async (ids, options = {}) => {
|
|||||||
|
|
||||||
const actors = await Promise.all(ids.map(async (id) => {
|
const actors = await Promise.all(ids.map(async (id) => {
|
||||||
try {
|
try {
|
||||||
const actor = (typeof id === 'object' && id.hasOwnProperty('id')) ? id : await activitypub.get(0, id);
|
const actor = (typeof id === 'object' && id.hasOwnProperty('id')) ? id : await activitypub.get('uid', 0, id);
|
||||||
|
|
||||||
// Follow counts
|
// Follow counts
|
||||||
try {
|
try {
|
||||||
const [followers, following] = await Promise.all([
|
const [followers, following] = await Promise.all([
|
||||||
actor.followers ? activitypub.get(0, actor.followers) : { totalItems: 0 },
|
actor.followers ? activitypub.get('uid', 0, actor.followers) : { totalItems: 0 },
|
||||||
actor.following ? activitypub.get(0, actor.following) : { totalItems: 0 },
|
actor.following ? activitypub.get('uid', 0, actor.following) : { totalItems: 0 },
|
||||||
]);
|
]);
|
||||||
actor.followerCount = followers.totalItems;
|
actor.followerCount = followers.totalItems;
|
||||||
actor.followingCount = following.totalItems;
|
actor.followingCount = following.totalItems;
|
||||||
@@ -46,7 +46,7 @@ Actors.assert = async (ids, options = {}) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Post count
|
// Post count
|
||||||
const outbox = actor.outbox ? await activitypub.get(0, actor.outbox) : { totalItems: 0 };
|
const outbox = actor.outbox ? await activitypub.get('uid', 0, actor.outbox) : { totalItems: 0 };
|
||||||
actor.postcount = outbox.totalItems;
|
actor.postcount = outbox.totalItems;
|
||||||
|
|
||||||
return actor;
|
return actor;
|
||||||
|
|||||||
@@ -63,8 +63,8 @@ Helpers.query = async (id) => {
|
|||||||
return { username, hostname, actorUri, publicKey };
|
return { username, hostname, actorUri, publicKey };
|
||||||
};
|
};
|
||||||
|
|
||||||
Helpers.generateKeys = async (uid) => {
|
Helpers.generateKeys = async (type, id) => {
|
||||||
winston.verbose(`[activitypub] Generating RSA key-pair for uid ${uid}`);
|
winston.verbose(`[activitypub] Generating RSA key-pair for ${type} ${id}`);
|
||||||
const {
|
const {
|
||||||
publicKey,
|
publicKey,
|
||||||
privateKey,
|
privateKey,
|
||||||
@@ -80,47 +80,41 @@ Helpers.generateKeys = async (uid) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await db.setObject(`uid:${uid}:keys`, { publicKey, privateKey });
|
await db.setObject(`${type}:${id}:keys`, { publicKey, privateKey });
|
||||||
return { publicKey, privateKey };
|
return { publicKey, privateKey };
|
||||||
};
|
};
|
||||||
|
|
||||||
Helpers.resolveLocalUid = async (input) => {
|
Helpers.resolveLocalId = async (input) => {
|
||||||
let slug;
|
|
||||||
const protocols = ['https'];
|
|
||||||
if (process.env.CI === 'true') {
|
|
||||||
protocols.push('http');
|
|
||||||
}
|
|
||||||
if (Helpers.isUri(input)) {
|
if (Helpers.isUri(input)) {
|
||||||
const { host, pathname } = new URL(input);
|
const { host, pathname } = new URL(input);
|
||||||
|
|
||||||
if (host === nconf.get('url_parsed').host) {
|
if (host === nconf.get('url_parsed').host) {
|
||||||
const [type, value] = pathname.replace(nconf.get('relative_path'), '').split('/').filter(Boolean);
|
const [prefix, value] = pathname.replace(nconf.get('relative_path'), '').split('/').filter(Boolean);
|
||||||
if (type === 'uid') {
|
|
||||||
return value;
|
switch (prefix) {
|
||||||
|
case 'uid':
|
||||||
|
return { type: 'user', id: value };
|
||||||
|
|
||||||
|
case 'post':
|
||||||
|
return { type: 'post', id: value };
|
||||||
|
|
||||||
|
case 'category':
|
||||||
|
return { type: 'category', id: value };
|
||||||
|
|
||||||
|
case 'user': {
|
||||||
|
const uid = await user.getUidByUserslug(value);
|
||||||
|
return { type: 'user', id: uid };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
slug = value;
|
throw new Error('[[error:activitypub.invalid-id]]');
|
||||||
} else {
|
} else {
|
||||||
throw new Error('[[error:activitypub.invalid-id]]');
|
throw new Error('[[error:activitypub.invalid-id]]');
|
||||||
}
|
}
|
||||||
} else if (input.indexOf('@') !== -1) { // Webfinger
|
} else if (input.indexOf('@') !== -1) { // Webfinger
|
||||||
([slug] = input.replace(/^acct:/, '').split('@'));
|
const [slug] = input.replace(/^acct:/, '').split('@');
|
||||||
} else {
|
const uid = await user.getUidByUserslug(slug);
|
||||||
throw new Error('[[error:activitypub.invalid-id]]');
|
return { type: 'user', id: uid };
|
||||||
}
|
|
||||||
|
|
||||||
return await user.getUidByUserslug(slug);
|
|
||||||
};
|
|
||||||
|
|
||||||
Helpers.resolveLocalPid = async (uri) => {
|
|
||||||
const { host, pathname } = new URL(uri);
|
|
||||||
if (host === nconf.get('url_parsed').host) {
|
|
||||||
const [type, value] = pathname.replace(nconf.get('relative_path'), '').split('/').filter(Boolean);
|
|
||||||
if (type !== 'post') {
|
|
||||||
throw new Error('[[error:activitypub.invalid-id]]');
|
|
||||||
}
|
|
||||||
|
|
||||||
return value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error('[[error:activitypub.invalid-id]]');
|
throw new Error('[[error:activitypub.invalid-id]]');
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ const winston = require('winston');
|
|||||||
const db = require('../database');
|
const db = require('../database');
|
||||||
const user = require('../user');
|
const user = require('../user');
|
||||||
const posts = require('../posts');
|
const posts = require('../posts');
|
||||||
|
const categories = require('../categories');
|
||||||
const activitypub = require('.');
|
const activitypub = require('.');
|
||||||
|
|
||||||
const helpers = require('./helpers');
|
const helpers = require('./helpers');
|
||||||
@@ -56,16 +57,19 @@ inbox.update = async (req) => {
|
|||||||
|
|
||||||
inbox.like = async (req) => {
|
inbox.like = async (req) => {
|
||||||
const { actor, object } = req.body;
|
const { actor, object } = req.body;
|
||||||
const pid = await activitypub.helpers.resolveLocalPid(object);
|
const { type, id } = await activitypub.helpers.resolveLocalId(object);
|
||||||
|
if (type !== 'post' || await posts.exists(id)) {
|
||||||
|
throw new Error('[[error:activitypub.invalid-id]]');
|
||||||
|
}
|
||||||
|
|
||||||
await posts.upvote(pid, actor);
|
await posts.upvote(id, actor);
|
||||||
};
|
};
|
||||||
|
|
||||||
inbox.follow = async (req) => {
|
inbox.follow = async (req) => {
|
||||||
// Sanity checks
|
// Sanity checks
|
||||||
const localUid = await helpers.resolveLocalUid(req.body.object);
|
const { type, id } = await helpers.resolveLocalId(req.body.object);
|
||||||
if (!localUid) {
|
if (!['category', 'user'].includes(type)) {
|
||||||
throw new Error('[[error:invalid-uid]]');
|
throw new Error('[[error:activitypub.invalid-id]]');
|
||||||
}
|
}
|
||||||
|
|
||||||
const assertion = await activitypub.actors.assert(req.body.actor);
|
const assertion = await activitypub.actors.assert(req.body.actor);
|
||||||
@@ -73,24 +77,53 @@ inbox.follow = async (req) => {
|
|||||||
throw new Error('[[error:activitypub.invalid-id]]');
|
throw new Error('[[error:activitypub.invalid-id]]');
|
||||||
}
|
}
|
||||||
|
|
||||||
const isFollowed = await inbox.isFollowed(req.body.actor, localUid);
|
if (type === 'user') {
|
||||||
if (isFollowed) {
|
const exists = await user.exists(id);
|
||||||
// No additional parsing required
|
if (!exists) {
|
||||||
return;
|
throw new Error('[[error:invalid-uid]]');
|
||||||
|
}
|
||||||
|
|
||||||
|
const isFollowed = await inbox.isFollowed(req.body.actor, id);
|
||||||
|
if (isFollowed) {
|
||||||
|
// No additional parsing required
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
await db.sortedSetAdd(`followersRemote:${id}`, now, req.body.actor);
|
||||||
|
|
||||||
|
const followerRemoteCount = await db.sortedSetCard(`followersRemote:${id}`);
|
||||||
|
await user.setUserField(id, 'followerRemoteCount', followerRemoteCount);
|
||||||
|
|
||||||
|
await activitypub.send('uid', id, req.body.actor, {
|
||||||
|
type: 'Accept',
|
||||||
|
object: {
|
||||||
|
type: 'Follow',
|
||||||
|
actor: req.body.actor,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else if (type === 'category') {
|
||||||
|
const exists = await categories.exists(id);
|
||||||
|
if (!exists) {
|
||||||
|
throw new Error('[[error:invalid-cid]]');
|
||||||
|
}
|
||||||
|
|
||||||
|
const watchState = await categories.getWatchState([id], req.body.actor);
|
||||||
|
if (watchState === categories.watchStates.tracking) {
|
||||||
|
// No additional parsing required
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await user.setCategoryWatchState(req.body.actor, id, categories.watchStates.tracking);
|
||||||
|
|
||||||
|
await activitypub.send('cid', id, req.body.actor, {
|
||||||
|
type: 'Accept',
|
||||||
|
object: {
|
||||||
|
type: 'Follow',
|
||||||
|
actor: req.body.actor,
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const now = Date.now();
|
|
||||||
await db.sortedSetAdd(`followersRemote:${localUid}`, now, req.body.actor);
|
|
||||||
await activitypub.send(localUid, req.body.actor, {
|
|
||||||
type: 'Accept',
|
|
||||||
object: {
|
|
||||||
type: 'Follow',
|
|
||||||
actor: req.body.actor,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const followerRemoteCount = await db.sortedSetCard(`followersRemote:${localUid}`);
|
|
||||||
await user.setUserField(localUid, 'followerRemoteCount', followerRemoteCount);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
inbox.isFollowed = async (actorId, uid) => {
|
inbox.isFollowed = async (actorId, uid) => {
|
||||||
@@ -104,8 +137,8 @@ inbox.accept = async (req) => {
|
|||||||
const { actor, object } = req.body;
|
const { actor, object } = req.body;
|
||||||
const { type } = object;
|
const { type } = object;
|
||||||
|
|
||||||
const uid = await helpers.resolveLocalUid(object.actor);
|
const { type: localType, id: uid } = await helpers.resolveLocalId(object.actor);
|
||||||
if (!uid) {
|
if (localType !== 'user' || !uid) {
|
||||||
throw new Error('[[error:invalid-uid]]');
|
throw new Error('[[error:invalid-uid]]');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,6 +157,7 @@ inbox.accept = async (req) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
inbox.undo = async (req) => {
|
inbox.undo = async (req) => {
|
||||||
|
// todo: "actor" in this case should be the one in object, no?
|
||||||
const { actor, object } = req.body;
|
const { actor, object } = req.body;
|
||||||
const { type } = object;
|
const { type } = object;
|
||||||
|
|
||||||
@@ -132,23 +166,45 @@ inbox.undo = async (req) => {
|
|||||||
throw new Error('[[error:activitypub.invalid-id]]');
|
throw new Error('[[error:activitypub.invalid-id]]');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { type: localType, id } = await helpers.resolveLocalId(object.object);
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'Follow': {
|
case 'Follow': {
|
||||||
const uid = await helpers.resolveLocalUid(object.object);
|
switch (localType) {
|
||||||
if (!uid) {
|
case 'user': {
|
||||||
throw new Error('[[error:invalid-uid]]');
|
const exists = await user.exists(id);
|
||||||
|
if (!exists) {
|
||||||
|
throw new Error('[[error:invalid-uid]]');
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
db.sortedSetRemove(`followersRemote:${id}`, actor),
|
||||||
|
db.decrObjectField(`user:${id}`, 'followerRemoteCount'),
|
||||||
|
]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'category': {
|
||||||
|
const exists = await categories.exists(id);
|
||||||
|
if (!exists) {
|
||||||
|
throw new Error('[[error:invalid-cid]]');
|
||||||
|
}
|
||||||
|
|
||||||
|
await user.setCategoryWatchState(actor, id, categories.watchStates.notwatching);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await Promise.all([
|
|
||||||
db.sortedSetRemove(`followersRemote:${uid}`, actor),
|
|
||||||
db.decrObjectField(`user:${uid}`, 'followerRemoteCount'),
|
|
||||||
]);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'Like': {
|
case 'Like': {
|
||||||
const pid = await helpers.resolveLocalPid(object.object);
|
const exists = await posts.exists(id);
|
||||||
await posts.unvote(pid, actor);
|
if (localType !== 'post' || !exists) {
|
||||||
|
throw new Error('[[error:invalid-pid]]');
|
||||||
|
}
|
||||||
|
|
||||||
|
await posts.unvote(id, actor);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,28 +37,40 @@ ActivityPub.resolveInboxes = async (ids) => {
|
|||||||
return Array.from(inboxes);
|
return Array.from(inboxes);
|
||||||
};
|
};
|
||||||
|
|
||||||
ActivityPub.getPublicKey = async (uid) => {
|
ActivityPub.getPublicKey = async (type, id) => {
|
||||||
let publicKey;
|
let publicKey;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
({ publicKey } = await db.getObject(`uid:${uid}:keys`));
|
({ publicKey } = await db.getObject(`uid:${id}:keys`));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
({ publicKey } = await ActivityPub.helpers.generateKeys(uid));
|
({ publicKey } = await ActivityPub.helpers.generateKeys(type, id));
|
||||||
}
|
}
|
||||||
|
|
||||||
return publicKey;
|
return publicKey;
|
||||||
};
|
};
|
||||||
|
|
||||||
ActivityPub.getPrivateKey = async (uid) => {
|
ActivityPub.getPrivateKey = async (type, id) => {
|
||||||
|
// Sanity checking
|
||||||
|
if (!['cid', 'uid'].includes(type) || !utils.isNumber(id) || parseInt(id, 10) < 0) {
|
||||||
|
throw new Error('[[error:invalid-data]]');
|
||||||
|
}
|
||||||
|
id = parseInt(id, 10);
|
||||||
let privateKey;
|
let privateKey;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
({ privateKey } = await db.getObject(`uid:${uid}:keys`));
|
({ privateKey } = await db.getObject(`${type}:${id}:keys`));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
({ privateKey } = await ActivityPub.helpers.generateKeys(uid));
|
({ privateKey } = await ActivityPub.helpers.generateKeys(type, id));
|
||||||
}
|
}
|
||||||
|
|
||||||
return privateKey;
|
let keyId;
|
||||||
|
if (type === 'uid') {
|
||||||
|
keyId = `${nconf.get('url')}${id > 0 ? `/uid/${id}` : '/actor'}#key`;
|
||||||
|
} else {
|
||||||
|
keyId = `${nconf.get('url')}/category/${id}#key`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { key: privateKey, keyId };
|
||||||
};
|
};
|
||||||
|
|
||||||
ActivityPub.fetchPublicKey = async (uri) => {
|
ActivityPub.fetchPublicKey = async (uri) => {
|
||||||
@@ -76,18 +88,10 @@ ActivityPub.fetchPublicKey = async (uri) => {
|
|||||||
return body.publicKey;
|
return body.publicKey;
|
||||||
};
|
};
|
||||||
|
|
||||||
ActivityPub.sign = async (uid, url, payload) => {
|
ActivityPub.sign = async ({ key, keyId }, url, payload) => {
|
||||||
// Sanity checking
|
|
||||||
if (!utils.isNumber(uid) || parseInt(uid, 10) < 0) {
|
|
||||||
throw new Error('[[error:invalid-uid]]');
|
|
||||||
}
|
|
||||||
uid = parseInt(uid, 10);
|
|
||||||
|
|
||||||
// Returns string for use in 'Signature' header
|
// Returns string for use in 'Signature' header
|
||||||
const { host, pathname } = new URL(url);
|
const { host, pathname } = new URL(url);
|
||||||
const date = new Date().toUTCString();
|
const date = new Date().toUTCString();
|
||||||
const key = await ActivityPub.getPrivateKey(uid);
|
|
||||||
const keyId = `${nconf.get('url')}${uid > 0 ? `/uid/${uid}` : '/actor'}#key`;
|
|
||||||
let digest = null;
|
let digest = null;
|
||||||
|
|
||||||
let headers = '(request-target) host date';
|
let headers = '(request-target) host date';
|
||||||
@@ -156,13 +160,14 @@ ActivityPub.verify = async (req) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
ActivityPub.get = async (uid, uri) => {
|
ActivityPub.get = async (type, id, uri) => {
|
||||||
const cacheKey = [uid, uri].join(';');
|
const cacheKey = [id, uri].join(';');
|
||||||
if (requestCache.has(cacheKey)) {
|
if (requestCache.has(cacheKey)) {
|
||||||
return requestCache.get(cacheKey);
|
return requestCache.get(cacheKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
const headers = uid >= 0 ? await ActivityPub.sign(uid, uri) : {};
|
const keyData = await ActivityPub.getPrivateKey(type, id);
|
||||||
|
const headers = id >= 0 ? await ActivityPub.sign(keyData, uri) : {};
|
||||||
winston.verbose(`[activitypub/get] ${uri}`);
|
winston.verbose(`[activitypub/get] ${uri}`);
|
||||||
const { response, body } = await request.get(uri, {
|
const { response, body } = await request.get(uri, {
|
||||||
headers: {
|
headers: {
|
||||||
@@ -184,7 +189,7 @@ ActivityPub.get = async (uid, uri) => {
|
|||||||
return body;
|
return body;
|
||||||
};
|
};
|
||||||
|
|
||||||
ActivityPub.send = async (uid, targets, payload) => {
|
ActivityPub.send = async (type, id, targets, payload) => {
|
||||||
if (!Array.isArray(targets)) {
|
if (!Array.isArray(targets)) {
|
||||||
targets = [targets];
|
targets = [targets];
|
||||||
}
|
}
|
||||||
@@ -193,12 +198,14 @@ ActivityPub.send = async (uid, targets, payload) => {
|
|||||||
|
|
||||||
payload = {
|
payload = {
|
||||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||||
actor: `${nconf.get('url')}/uid/${uid}`,
|
actor: `${nconf.get('url')}/uid/${id}`,
|
||||||
...payload,
|
...payload,
|
||||||
};
|
};
|
||||||
|
|
||||||
await Promise.all(inboxes.map(async (uri) => {
|
await Promise.all(inboxes.map(async (uri) => {
|
||||||
const headers = await ActivityPub.sign(uid, uri, payload);
|
const keyData = await ActivityPub.getPrivateKey(type, id);
|
||||||
|
const headers = await ActivityPub.sign(keyData, uri, payload);
|
||||||
|
winston.verbose(`[activitypub/send] ${uri}`);
|
||||||
const { response } = await request.post(uri, {
|
const { response } = await request.post(uri, {
|
||||||
headers: {
|
headers: {
|
||||||
...headers,
|
...headers,
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ Mocks.actors = {};
|
|||||||
|
|
||||||
Mocks.actors.user = async (uid) => {
|
Mocks.actors.user = async (uid) => {
|
||||||
let { username, userslug, displayname: name, aboutme, picture, 'cover:url': cover } = await user.getUserData(uid);
|
let { username, userslug, displayname: name, aboutme, picture, 'cover:url': cover } = await user.getUserData(uid);
|
||||||
const publicKey = await activitypub.getPublicKey(uid);
|
const publicKey = await activitypub.getPublicKey('uid', uid);
|
||||||
|
|
||||||
if (picture) {
|
if (picture) {
|
||||||
const imagePath = await user.getLocalAvatarPath(uid);
|
const imagePath = await user.getLocalAvatarPath(uid);
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ const activitypub = module.parent.exports;
|
|||||||
const Notes = module.exports;
|
const Notes = module.exports;
|
||||||
|
|
||||||
Notes.resolveId = async (uid, id) => {
|
Notes.resolveId = async (uid, id) => {
|
||||||
({ id } = await activitypub.get(uid, id));
|
({ id } = await activitypub.get('uid', uid, id));
|
||||||
return id;
|
return id;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ Notes.assert = async (uid, input, options = {}) => {
|
|||||||
let postData;
|
let postData;
|
||||||
winston.verbose(`[activitypub/notes.assert] Not found, saving note to database`);
|
winston.verbose(`[activitypub/notes.assert] Not found, saving note to database`);
|
||||||
if (activitypub.helpers.isUri(item)) {
|
if (activitypub.helpers.isUri(item)) {
|
||||||
const object = await activitypub.get(uid, item);
|
const object = await activitypub.get('uid', uid, item);
|
||||||
postData = await activitypub.mocks.post(object);
|
postData = await activitypub.mocks.post(object);
|
||||||
} else {
|
} else {
|
||||||
postData = item;
|
postData = item;
|
||||||
@@ -60,7 +60,7 @@ Notes.getParentChain = async (uid, input) => {
|
|||||||
await traverse(uid, postData.toPid);
|
await traverse(uid, postData.toPid);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let object = await activitypub.get(uid, id);
|
let object = await activitypub.get('uid', uid, id);
|
||||||
object = await activitypub.mocks.post(object);
|
object = await activitypub.mocks.post(object);
|
||||||
if (object) {
|
if (object) {
|
||||||
chain.add(object);
|
chain.add(object);
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ activitypubApi.follow = async (caller, { uid } = {}) => {
|
|||||||
throw new Error('[[error:activitypub.invalid-id]]');
|
throw new Error('[[error:activitypub.invalid-id]]');
|
||||||
}
|
}
|
||||||
|
|
||||||
await activitypub.send(caller.uid, [result.actorUri], {
|
await activitypub.send('uid', caller.uid, [result.actorUri], {
|
||||||
type: 'Follow',
|
type: 'Follow',
|
||||||
object: result.actorUri,
|
object: result.actorUri,
|
||||||
});
|
});
|
||||||
@@ -35,7 +35,7 @@ activitypubApi.unfollow = async (caller, { uid }) => {
|
|||||||
throw new Error('[[error:activitypub.invalid-id]]');
|
throw new Error('[[error:activitypub.invalid-id]]');
|
||||||
}
|
}
|
||||||
|
|
||||||
await activitypub.send(caller.uid, [result.actorUri], {
|
await activitypub.send('uid', caller.uid, [result.actorUri], {
|
||||||
type: 'Undo',
|
type: 'Undo',
|
||||||
object: {
|
object: {
|
||||||
type: 'Follow',
|
type: 'Follow',
|
||||||
@@ -81,7 +81,7 @@ activitypubApi.create.post = async (caller, { pid }) => {
|
|||||||
object,
|
object,
|
||||||
};
|
};
|
||||||
|
|
||||||
await activitypub.send(caller.uid, Array.from(targets), payload);
|
await activitypub.send('uid', caller.uid, Array.from(targets), payload);
|
||||||
};
|
};
|
||||||
|
|
||||||
activitypubApi.update = {};
|
activitypubApi.update = {};
|
||||||
@@ -92,7 +92,7 @@ activitypubApi.update.profile = async (caller, { uid }) => {
|
|||||||
db.getSortedSetMembers(`followersRemote:${caller.uid}`),
|
db.getSortedSetMembers(`followersRemote:${caller.uid}`),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
await activitypub.send(caller.uid, followers, {
|
await activitypub.send('uid', caller.uid, followers, {
|
||||||
type: 'Update',
|
type: 'Update',
|
||||||
to: [activitypub._constants.publicAddress],
|
to: [activitypub._constants.publicAddress],
|
||||||
cc: [],
|
cc: [],
|
||||||
@@ -111,7 +111,7 @@ activitypubApi.update.note = async (caller, { post }) => {
|
|||||||
object,
|
object,
|
||||||
};
|
};
|
||||||
|
|
||||||
await activitypub.send(caller.uid, Array.from(targets), payload);
|
await activitypub.send('uid', caller.uid, Array.from(targets), payload);
|
||||||
};
|
};
|
||||||
|
|
||||||
activitypubApi.like = {};
|
activitypubApi.like = {};
|
||||||
@@ -126,7 +126,7 @@ activitypubApi.like.note = async (caller, { pid }) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await activitypub.send(caller.uid, [uid], {
|
await activitypub.send('uid', caller.uid, [uid], {
|
||||||
type: 'Like',
|
type: 'Like',
|
||||||
object: pid,
|
object: pid,
|
||||||
});
|
});
|
||||||
@@ -146,7 +146,7 @@ activitypubApi.undo.like = async (caller, { pid }) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await activitypub.send(caller.uid, [uid], {
|
await activitypub.send('uid', caller.uid, [uid], {
|
||||||
type: 'Undo',
|
type: 'Undo',
|
||||||
object: {
|
object: {
|
||||||
actor: `${nconf.get('url')}/uid/${caller.uid}`,
|
actor: `${nconf.get('url')}/uid/${caller.uid}`,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
const db = require('../database');
|
const db = require('../database');
|
||||||
const user = require('../user');
|
const user = require('../user');
|
||||||
|
const activitypub = require('../activitypub');
|
||||||
|
|
||||||
module.exports = function (Categories) {
|
module.exports = function (Categories) {
|
||||||
Categories.watchStates = {
|
Categories.watchStates = {
|
||||||
@@ -20,7 +21,7 @@ module.exports = function (Categories) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
Categories.getWatchState = async function (cids, uid) {
|
Categories.getWatchState = async function (cids, uid) {
|
||||||
if (!(parseInt(uid, 10) > 0)) {
|
if (!activitypub.helpers.isUri(uid) && !(parseInt(uid, 10) > 0)) {
|
||||||
return cids.map(() => Categories.watchStates.notwatching);
|
return cids.map(() => Categories.watchStates.notwatching);
|
||||||
}
|
}
|
||||||
if (!Array.isArray(cids) || !cids.length) {
|
if (!Array.isArray(cids) || !cids.length) {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ const activitypub = require('../../activitypub');
|
|||||||
const Actors = module.exports;
|
const Actors = module.exports;
|
||||||
|
|
||||||
Actors.application = async function (req, res) {
|
Actors.application = async function (req, res) {
|
||||||
const publicKey = await activitypub.getPublicKey(0);
|
const publicKey = await activitypub.getPublicKey('uid', 0);
|
||||||
const name = meta.config.title || 'NodeBB';
|
const name = meta.config.title || 'NodeBB';
|
||||||
|
|
||||||
res.status(200).json({
|
res.status(200).json({
|
||||||
|
|||||||
@@ -4,11 +4,12 @@ const _ = require('lodash');
|
|||||||
|
|
||||||
const db = require('../database');
|
const db = require('../database');
|
||||||
const categories = require('../categories');
|
const categories = require('../categories');
|
||||||
|
const activitypub = require('../activitypub');
|
||||||
const plugins = require('../plugins');
|
const plugins = require('../plugins');
|
||||||
|
|
||||||
module.exports = function (User) {
|
module.exports = function (User) {
|
||||||
User.setCategoryWatchState = async function (uid, cids, state) {
|
User.setCategoryWatchState = async function (uid, cids, state) {
|
||||||
if (!(parseInt(uid, 10) > 0)) {
|
if (!activitypub.helpers.isUri(uid) && !(parseInt(uid, 10) > 0)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const isStateValid = Object.values(categories.watchStates).includes(parseInt(state, 10));
|
const isStateValid = Object.values(categories.watchStates).includes(parseInt(state, 10));
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ const validator = require('validator');
|
|||||||
|
|
||||||
const meta = require('../meta');
|
const meta = require('../meta');
|
||||||
const db = require('../database');
|
const db = require('../database');
|
||||||
|
const activitypub = require('../activitypub');
|
||||||
const plugins = require('../plugins');
|
const plugins = require('../plugins');
|
||||||
const notifications = require('../notifications');
|
const notifications = require('../notifications');
|
||||||
const languages = require('../languages');
|
const languages = require('../languages');
|
||||||
@@ -16,6 +17,10 @@ module.exports = function (User) {
|
|||||||
postsPerPage: 20,
|
postsPerPage: 20,
|
||||||
topicsPerPage: 20,
|
topicsPerPage: 20,
|
||||||
};
|
};
|
||||||
|
const remoteDefaultSettings = Object.freeze({
|
||||||
|
categoryWatchState: 'notwatching',
|
||||||
|
});
|
||||||
|
|
||||||
User.getSettings = async function (uid) {
|
User.getSettings = async function (uid) {
|
||||||
if (parseInt(uid, 10) <= 0) {
|
if (parseInt(uid, 10) <= 0) {
|
||||||
const isSpider = parseInt(uid, 10) === -1;
|
const isSpider = parseInt(uid, 10) === -1;
|
||||||
@@ -90,6 +95,8 @@ module.exports = function (User) {
|
|||||||
function getSetting(settings, key, defaultValue) {
|
function getSetting(settings, key, defaultValue) {
|
||||||
if (settings[key] || settings[key] === 0) {
|
if (settings[key] || settings[key] === 0) {
|
||||||
return settings[key];
|
return settings[key];
|
||||||
|
} else if (activitypub.helpers.isUri(settings.uid) && remoteDefaultSettings[key]) {
|
||||||
|
return remoteDefaultSettings[key];
|
||||||
} else if (meta.config[key] || meta.config[key] === 0) {
|
} else if (meta.config[key] || meta.config[key] === 0) {
|
||||||
return meta.config[key];
|
return meta.config[key];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ describe('ActivityPub integration', () => {
|
|||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('.resolveLocalUid()', () => {
|
describe('.resolveLocalId()', () => {
|
||||||
let uid;
|
let uid;
|
||||||
let slug;
|
let slug;
|
||||||
|
|
||||||
@@ -99,29 +99,29 @@ describe('ActivityPub integration', () => {
|
|||||||
|
|
||||||
it('should throw when an invalid input is passed in', async () => {
|
it('should throw when an invalid input is passed in', async () => {
|
||||||
await assert.rejects(
|
await assert.rejects(
|
||||||
activitypub.helpers.resolveLocalUid('ncl28h3qwhoiclwnevoinw3u'),
|
activitypub.helpers.resolveLocalId('ncl28h3qwhoiclwnevoinw3u'),
|
||||||
{ message: '[[error:activitypub.invalid-id]]' }
|
{ message: '[[error:activitypub.invalid-id]]' }
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return null when valid input is passed but does not resolve', async () => {
|
it('should return null when valid input is passed but does not resolve', async () => {
|
||||||
const uid = await activitypub.helpers.resolveLocalUid(`acct:foobar@${nconf.get('url_parsed').host}`);
|
const { id } = await activitypub.helpers.resolveLocalId(`acct:foobar@${nconf.get('url_parsed').host}`);
|
||||||
assert.strictEqual(uid, null);
|
assert.strictEqual(id, null);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should resolve to a local uid when given a webfinger-style string', async () => {
|
it('should resolve to a local uid when given a webfinger-style string', async () => {
|
||||||
const found = await activitypub.helpers.resolveLocalUid(`acct:${slug}@${nconf.get('url_parsed').host}`);
|
const { id } = await activitypub.helpers.resolveLocalId(`acct:${slug}@${nconf.get('url_parsed').host}`);
|
||||||
assert.strictEqual(found, uid);
|
assert.strictEqual(id, uid);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should resolve even without the "acct:" prefix', async () => {
|
it('should resolve even without the "acct:" prefix', async () => {
|
||||||
const found = await activitypub.helpers.resolveLocalUid(`${slug}@${nconf.get('url_parsed').host}`);
|
const { id } = await activitypub.helpers.resolveLocalId(`${slug}@${nconf.get('url_parsed').host}`);
|
||||||
assert.strictEqual(found, uid);
|
assert.strictEqual(id, uid);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should resolve when passed a full URL', async () => {
|
it('should resolve when passed a full URL', async () => {
|
||||||
const found = await activitypub.helpers.resolveLocalUid(`${nconf.get('url')}/user/${slug}`);
|
const { id } = await activitypub.helpers.resolveLocalId(`${nconf.get('url')}/user/${slug}`);
|
||||||
assert.strictEqual(found, uid);
|
assert.strictEqual(id, uid);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -274,7 +274,8 @@ describe('ActivityPub integration', () => {
|
|||||||
|
|
||||||
it('should create a key-pair for a user if the user does not have one already', async () => {
|
it('should create a key-pair for a user if the user does not have one already', async () => {
|
||||||
const endpoint = `${nconf.get('url')}/uid/${uid}/inbox`;
|
const endpoint = `${nconf.get('url')}/uid/${uid}/inbox`;
|
||||||
await activitypub.sign(uid, endpoint);
|
const keyData = await activitypub.getPrivateKey('uid', uid);
|
||||||
|
await activitypub.sign(keyData, endpoint);
|
||||||
const { publicKey, privateKey } = await db.getObject(`uid:${uid}:keys`);
|
const { publicKey, privateKey } = await db.getObject(`uid:${uid}:keys`);
|
||||||
|
|
||||||
assert(publicKey);
|
assert(publicKey);
|
||||||
@@ -283,7 +284,8 @@ describe('ActivityPub integration', () => {
|
|||||||
|
|
||||||
it('should return an object with date, a null digest, and signature, if no payload is passed in', async () => {
|
it('should return an object with date, a null digest, and signature, if no payload is passed in', async () => {
|
||||||
const endpoint = `${nconf.get('url')}/uid/${uid}/inbox`;
|
const endpoint = `${nconf.get('url')}/uid/${uid}/inbox`;
|
||||||
const { date, digest, signature } = await activitypub.sign(uid, endpoint);
|
const keyData = await activitypub.getPrivateKey('uid', uid);
|
||||||
|
const { date, digest, signature } = await activitypub.sign(keyData, endpoint);
|
||||||
const dateObj = new Date(date);
|
const dateObj = new Date(date);
|
||||||
|
|
||||||
assert(signature);
|
assert(signature);
|
||||||
@@ -294,7 +296,8 @@ describe('ActivityPub integration', () => {
|
|||||||
it('should also return a digest hash if payload is passed in', async () => {
|
it('should also return a digest hash if payload is passed in', async () => {
|
||||||
const endpoint = `${nconf.get('url')}/uid/${uid}/inbox`;
|
const endpoint = `${nconf.get('url')}/uid/${uid}/inbox`;
|
||||||
const payload = { foo: 'bar' };
|
const payload = { foo: 'bar' };
|
||||||
const { digest } = await activitypub.sign(uid, endpoint, payload);
|
const keyData = await activitypub.getPrivateKey('uid', uid);
|
||||||
|
const { digest } = await activitypub.sign(keyData, endpoint, payload);
|
||||||
const hash = createHash('sha256');
|
const hash = createHash('sha256');
|
||||||
hash.update(JSON.stringify(payload));
|
hash.update(JSON.stringify(payload));
|
||||||
const checksum = hash.digest('base64');
|
const checksum = hash.digest('base64');
|
||||||
@@ -305,7 +308,8 @@ describe('ActivityPub integration', () => {
|
|||||||
|
|
||||||
it('should create a key for NodeBB itself if a uid of 0 is passed in', async () => {
|
it('should create a key for NodeBB itself if a uid of 0 is passed in', async () => {
|
||||||
const endpoint = `${nconf.get('url')}/uid/${uid}/inbox`;
|
const endpoint = `${nconf.get('url')}/uid/${uid}/inbox`;
|
||||||
await activitypub.sign(0, endpoint);
|
const keyData = await activitypub.getPrivateKey('uid', 0);
|
||||||
|
await activitypub.sign(keyData, endpoint);
|
||||||
const { publicKey, privateKey } = await db.getObject(`uid:0:keys`);
|
const { publicKey, privateKey } = await db.getObject(`uid:0:keys`);
|
||||||
|
|
||||||
assert(publicKey);
|
assert(publicKey);
|
||||||
@@ -314,7 +318,8 @@ describe('ActivityPub integration', () => {
|
|||||||
|
|
||||||
it('should return headers with an appropriate key id uri', async () => {
|
it('should return headers with an appropriate key id uri', async () => {
|
||||||
const endpoint = `${nconf.get('url')}/uid/${uid}/inbox`;
|
const endpoint = `${nconf.get('url')}/uid/${uid}/inbox`;
|
||||||
const { signature } = await activitypub.sign(uid, endpoint);
|
const keyData = await activitypub.getPrivateKey('uid', uid);
|
||||||
|
const { signature } = await activitypub.sign(keyData, endpoint);
|
||||||
const [keyId] = signature.split(',');
|
const [keyId] = signature.split(',');
|
||||||
|
|
||||||
assert(signature);
|
assert(signature);
|
||||||
@@ -323,7 +328,8 @@ describe('ActivityPub integration', () => {
|
|||||||
|
|
||||||
it('should return the instance key id when uid is 0', async () => {
|
it('should return the instance key id when uid is 0', async () => {
|
||||||
const endpoint = `${nconf.get('url')}/uid/${uid}/inbox`;
|
const endpoint = `${nconf.get('url')}/uid/${uid}/inbox`;
|
||||||
const { signature } = await activitypub.sign(0, endpoint);
|
const keyData = await activitypub.getPrivateKey('uid', 0);
|
||||||
|
const { signature } = await activitypub.sign(keyData, endpoint);
|
||||||
const [keyId] = signature.split(',');
|
const [keyId] = signature.split(',');
|
||||||
|
|
||||||
assert(signature);
|
assert(signature);
|
||||||
@@ -355,7 +361,8 @@ describe('ActivityPub integration', () => {
|
|||||||
it('should return true when the proper signature and relevant headers are passed in', async () => {
|
it('should return true when the proper signature and relevant headers are passed in', async () => {
|
||||||
const endpoint = `${nconf.get('url')}/user/${username}/inbox`;
|
const endpoint = `${nconf.get('url')}/user/${username}/inbox`;
|
||||||
const path = `/user/${username}/inbox`;
|
const path = `/user/${username}/inbox`;
|
||||||
const signature = await activitypub.sign(uid, endpoint);
|
const keyData = await activitypub.getPrivateKey('uid', uid);
|
||||||
|
const signature = await activitypub.sign(keyData, endpoint);
|
||||||
const { host } = nconf.get('url_parsed');
|
const { host } = nconf.get('url_parsed');
|
||||||
const req = {
|
const req = {
|
||||||
...mockReqBase,
|
...mockReqBase,
|
||||||
@@ -372,7 +379,8 @@ describe('ActivityPub integration', () => {
|
|||||||
it('should return true when a digest is also passed in', async () => {
|
it('should return true when a digest is also passed in', async () => {
|
||||||
const endpoint = `${nconf.get('url')}/user/${username}/inbox`;
|
const endpoint = `${nconf.get('url')}/user/${username}/inbox`;
|
||||||
const path = `/user/${username}/inbox`;
|
const path = `/user/${username}/inbox`;
|
||||||
const signature = await activitypub.sign(uid, endpoint, { foo: 'bar' });
|
const keyData = await activitypub.getPrivateKey('uid', uid);
|
||||||
|
const signature = await activitypub.sign(keyData, endpoint, { foo: 'bar' });
|
||||||
const { host } = nconf.get('url_parsed');
|
const { host } = nconf.get('url_parsed');
|
||||||
const req = {
|
const req = {
|
||||||
...mockReqBase,
|
...mockReqBase,
|
||||||
@@ -412,7 +420,7 @@ describe('ActivityPub integration', () => {
|
|||||||
const post = (await posts.getPostSummaryByPids([postData.pid], uid, { stripTags: false })).pop();
|
const post = (await posts.getPostSummaryByPids([postData.pid], uid, { stripTags: false })).pop();
|
||||||
note = await activitypub.mocks.note(post);
|
note = await activitypub.mocks.note(post);
|
||||||
|
|
||||||
await activitypub.send(uid, [`${nconf.get('url')}/uid/${uid}`], {
|
await activitypub.send('uid', uid, [`${nconf.get('url')}/uid/${uid}`], {
|
||||||
type: 'Create',
|
type: 'Create',
|
||||||
object: note,
|
object: note,
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user