mirror of
https://github.com/NodeBB/NodeBB.git
synced 2025-12-24 09:20:32 +01:00
Compare commits
8 Commits
renovate/s
...
relays
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a4f0def72 | ||
|
|
e6c43e36f1 | ||
|
|
608c1a4df5 | ||
|
|
0311bba67e | ||
|
|
6a38f8f666 | ||
|
|
40040a4642 | ||
|
|
3d4863d388 | ||
|
|
cc8a338d9a |
@@ -28,6 +28,17 @@
|
||||
"rules.value": "Value",
|
||||
"rules.cid": "Category",
|
||||
|
||||
"relays": "Relays",
|
||||
"relays.intro": "A relay improves discovery of content to and from your NodeBB. Subscribing to a relay means content received by the relay is forwarded here, and content posted here is syndicated outward by the relay.",
|
||||
"relays.warning": "Note: Relays can send larges amounts of traffic in, and may increase storage and processing costs.",
|
||||
"relays.litepub": "NodeBB follows the LitePub-style relay standard. The URL you enter here should end with <code>/actor</code>.",
|
||||
"relays.add": "Add New Relay",
|
||||
"relays.relay": "Relay",
|
||||
"relays.state": "State",
|
||||
"relays.state-0": "Pending",
|
||||
"relays.state-1": "Receiving only",
|
||||
"relays.state-2": "Active",
|
||||
|
||||
"server-filtering": "Filtering",
|
||||
"count": "This NodeBB is currently aware of <strong>%1</strong> server(s)",
|
||||
"server.filter-help": "Specify servers you would like to bar from federating with your NodeBB. Alternatively, you may opt to selectively <em>allow</em> federation with specific servers, instead. Both options are supported, although they are mutually exclusive.",
|
||||
|
||||
@@ -5,7 +5,8 @@ define('admin/settings/activitypub', [
|
||||
'bootbox',
|
||||
'categorySelector',
|
||||
'api',
|
||||
], function (Benchpress, bootbox, categorySelector, api) {
|
||||
'alerts',
|
||||
], function (Benchpress, bootbox, categorySelector, api, alerts) {
|
||||
const Module = {};
|
||||
|
||||
Module.init = function () {
|
||||
@@ -29,7 +30,34 @@ define('admin/settings/activitypub', [
|
||||
if (tbodyEl) {
|
||||
tbodyEl.innerHTML = html;
|
||||
}
|
||||
});
|
||||
}).catch(alerts.error);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const relaysEl = document.getElementById('relays');
|
||||
if (relaysEl) {
|
||||
relaysEl.addEventListener('click', (e) => {
|
||||
const subselector = e.target.closest('[data-action]');
|
||||
if (subselector) {
|
||||
const action = subselector.getAttribute('data-action');
|
||||
switch (action) {
|
||||
case 'relays.add': {
|
||||
Module.throwRelaysModal();
|
||||
break;
|
||||
}
|
||||
|
||||
case 'relays.remove': {
|
||||
const url = subselector.closest('tr').getAttribute('data-url');
|
||||
api.del(`/admin/activitypub/relays/${encodeURIComponent(url)}`, {}).then(async (data) => {
|
||||
const html = await app.parseAndTranslate('admin/settings/activitypub', 'relays', { relays: data });
|
||||
const tbodyEl = document.querySelector('#relays tbody');
|
||||
if (tbodyEl) {
|
||||
$(tbodyEl).html(html);
|
||||
}
|
||||
}).catch(alerts.error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -49,7 +77,7 @@ define('admin/settings/activitypub', [
|
||||
if (tbodyEl) {
|
||||
tbodyEl.innerHTML = html;
|
||||
}
|
||||
});
|
||||
}).catch(alerts.error);
|
||||
};
|
||||
const modal = bootbox.dialog({
|
||||
title: '[[admin/settings/activitypub:rules.add]]',
|
||||
@@ -63,6 +91,10 @@ define('admin/settings/activitypub', [
|
||||
},
|
||||
});
|
||||
|
||||
modal.on('shown.bs.modal', function () {
|
||||
modal.find('input').focus();
|
||||
});
|
||||
|
||||
// category switcher
|
||||
categorySelector.init(modal.find('[component="category-selector"]'), {
|
||||
onSelect: function (selectedCategory) {
|
||||
@@ -75,5 +107,37 @@ define('admin/settings/activitypub', [
|
||||
});
|
||||
};
|
||||
|
||||
Module.throwRelaysModal = function () {
|
||||
Benchpress.render('admin/partials/activitypub/relays', {}).then(function (html) {
|
||||
const submit = function () {
|
||||
const formEl = modal.find('form').get(0);
|
||||
const payload = Object.fromEntries(new FormData(formEl));
|
||||
|
||||
api.post('/admin/activitypub/relays', payload).then(async (data) => {
|
||||
const html = await app.parseAndTranslate('admin/settings/activitypub', 'relays', { relays: data });
|
||||
const tbodyEl = document.querySelector('#relays tbody');
|
||||
if (tbodyEl) {
|
||||
$(tbodyEl).html(html);
|
||||
}
|
||||
}).catch(alerts.error);
|
||||
};
|
||||
const modal = bootbox.dialog({
|
||||
title: '[[admin/settings/activitypub:relays.add]]',
|
||||
message: html,
|
||||
buttons: {
|
||||
save: {
|
||||
label: '[[global:save]]',
|
||||
className: 'btn-primary',
|
||||
callback: submit,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
modal.on('shown.bs.modal', function () {
|
||||
modal.find('input').focus();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return Module;
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@ const nconf = require('nconf');
|
||||
|
||||
const posts = require('../posts');
|
||||
const utils = require('../utils');
|
||||
const { default: PG } = require('pg');
|
||||
|
||||
const activitypub = module.parent.exports;
|
||||
const Feps = module.exports;
|
||||
@@ -18,21 +19,29 @@ Feps.announce = async function announce(id, activity) {
|
||||
return;
|
||||
}
|
||||
|
||||
let relays = await activitypub.relays.list();
|
||||
relays = relays.reduce((memo, { state, url }) => {
|
||||
if (state === 2) {
|
||||
memo.push(url);
|
||||
}
|
||||
return memo;
|
||||
}, []);
|
||||
const followers = await activitypub.notes.getCategoryFollowers(cid);
|
||||
if (!followers.length) {
|
||||
const targets = relays.concat(followers);
|
||||
if (!targets.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { actor } = activity;
|
||||
if (actor && !actor.startsWith(nconf.get('url'))) {
|
||||
followers.unshift(actor);
|
||||
targets.unshift(actor);
|
||||
}
|
||||
const now = Date.now();
|
||||
if (activity.type === 'Create') {
|
||||
const isMain = await posts.isMain(localId || id);
|
||||
if (isMain) {
|
||||
activitypub.helpers.log(`[activitypub/inbox.announce(1b12)] Announcing plain object (${activity.id}) to followers of cid ${cid}`);
|
||||
await activitypub.send('cid', cid, followers, {
|
||||
activitypub.helpers.log(`[activitypub/inbox.announce(1b12)] Announcing plain object (${activity.id}) to followers of cid ${cid} and ${relays.length} relays`);
|
||||
await activitypub.send('cid', cid, targets, {
|
||||
id: `${nconf.get('url')}/post/${encodeURIComponent(id)}#activity/announce/${now}`,
|
||||
type: 'Announce',
|
||||
actor: `${nconf.get('url')}/category/${cid}`,
|
||||
@@ -43,8 +52,8 @@ Feps.announce = async function announce(id, activity) {
|
||||
}
|
||||
}
|
||||
|
||||
activitypub.helpers.log(`[activitypub/inbox.announce(1b12)] Announcing ${activity.type} (${activity.id}) to followers of cid ${cid}`);
|
||||
await activitypub.send('cid', cid, followers, {
|
||||
activitypub.helpers.log(`[activitypub/inbox.announce(1b12)] Announcing ${activity.type} (${activity.id}) to followers of cid ${cid} and ${relays.length} relays`);
|
||||
await activitypub.send('cid', cid, targets, {
|
||||
id: `${nconf.get('url')}/post/${encodeURIComponent(id)}#activity/announce/${now + 1}`,
|
||||
type: 'Announce',
|
||||
actor: `${nconf.get('url')}/category/${cid}`,
|
||||
|
||||
@@ -38,7 +38,7 @@ Helpers._test = (method, args) => {
|
||||
}, 2500);
|
||||
};
|
||||
// process.nextTick(() => {
|
||||
// Helpers._test(activitypub.notes.assert, [1, `https://`]);
|
||||
// Helpers._test(activitypub.relays.add, ['https://relay.publicsquare.global/actor']);
|
||||
// });
|
||||
let _lastLog;
|
||||
Helpers.log = (message) => {
|
||||
@@ -129,9 +129,12 @@ Helpers.query = async (id) => {
|
||||
({ href: actorUri } = actorUri);
|
||||
}
|
||||
|
||||
const { subject, publicKey } = body;
|
||||
let { subject, publicKey } = body;
|
||||
// Fix missing scheme
|
||||
if (!subject.startsWith('acct:') && !subject.startsWith('did:')) {
|
||||
subject = `acct:${subject}`;
|
||||
}
|
||||
const payload = { subject, username, hostname, actorUri, publicKey };
|
||||
|
||||
const claimedId = new URL(subject).pathname;
|
||||
webfingerCache.set(claimedId, payload);
|
||||
if (claimedId !== id) {
|
||||
@@ -193,6 +196,9 @@ Helpers.resolveLocalId = async (input) => {
|
||||
|
||||
case 'message':
|
||||
return { type: 'message', id: value, ...activityData };
|
||||
|
||||
case 'actor':
|
||||
return { type: 'application', id: null };
|
||||
}
|
||||
|
||||
return { type: null, id: null, ...activityData };
|
||||
|
||||
@@ -288,6 +288,9 @@ inbox.announce = async (req) => {
|
||||
cid = actor;
|
||||
}
|
||||
|
||||
// Received via relay?
|
||||
const fromRelay = await activitypub.relays.is(actor);
|
||||
|
||||
switch(true) {
|
||||
case object.type === 'Like': {
|
||||
const id = object.object.id || object.object;
|
||||
@@ -333,7 +336,7 @@ inbox.announce = async (req) => {
|
||||
socketHelpers.sendNotificationToPostOwner(pid, actor, 'announce', 'notifications:activitypub.announce');
|
||||
} else { // Remote object
|
||||
// Follower check
|
||||
if (!cid) {
|
||||
if (!fromRelay && !cid) {
|
||||
const { followers } = await activitypub.actors.getLocalFollowCounts(actor);
|
||||
if (!followers) {
|
||||
winston.verbose(`[activitypub/inbox.announce] Rejecting ${object.id} via ${actor} due to no followers`);
|
||||
@@ -367,9 +370,12 @@ inbox.announce = async (req) => {
|
||||
|
||||
inbox.follow = async (req) => {
|
||||
const { actor, object, id: followId } = req.body;
|
||||
|
||||
// Sanity checks
|
||||
const { type, id } = await helpers.resolveLocalId(object.id);
|
||||
if (!['category', 'user'].includes(type)) {
|
||||
if (type === 'application') {
|
||||
return activitypub.relays.handshake(req.body);
|
||||
} else if (!['category', 'user'].includes(type)) {
|
||||
throw new Error('[[error:activitypub.invalid-id]]');
|
||||
}
|
||||
|
||||
@@ -454,7 +460,9 @@ inbox.accept = async (req) => {
|
||||
const { type } = object;
|
||||
|
||||
const { type: localType, id } = await helpers.resolveLocalId(object.actor);
|
||||
if (!['user', 'category'].includes(localType)) {
|
||||
if (object.id === `${nconf.get('url')}/actor`) {
|
||||
return activitypub.relays.handshake(req.body);
|
||||
} else if (!['user', 'category'].includes(localType)) {
|
||||
throw new Error('[[error:invalid-data]]');
|
||||
}
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ ActivityPub._constants = Object.freeze({
|
||||
],
|
||||
acceptableActorTypes: new Set(['Application', 'Organization', 'Person', 'Service']),
|
||||
acceptableGroupTypes: new Set(['Group']),
|
||||
requiredActorProps: ['inbox', 'outbox'],
|
||||
requiredActorProps: ['inbox'],
|
||||
acceptedProtocols: ['https', ...(process.env.CI === 'true' ? ['http'] : [])],
|
||||
acceptable: {
|
||||
customFields: new Set(['PropertyValue', 'Link', 'Note']),
|
||||
@@ -65,6 +65,7 @@ ActivityPub.actors = require('./actors');
|
||||
ActivityPub.instances = require('./instances');
|
||||
ActivityPub.feps = require('./feps');
|
||||
ActivityPub.rules = require('./rules');
|
||||
ActivityPub.relays = require('./relays');
|
||||
|
||||
ActivityPub.startJobs = () => {
|
||||
ActivityPub.helpers.log('[activitypub/jobs] Registering jobs.');
|
||||
|
||||
126
src/activitypub/relays.js
Normal file
126
src/activitypub/relays.js
Normal file
@@ -0,0 +1,126 @@
|
||||
'use strict';
|
||||
|
||||
const nconf = require('nconf');
|
||||
|
||||
const db = require('../database');
|
||||
|
||||
const activitypub = module.parent.exports;
|
||||
const Relays = module.exports;
|
||||
|
||||
Relays.is = async (actor) => {
|
||||
return db.isSortedSetMember('relays:createtime', actor);
|
||||
};
|
||||
|
||||
Relays.list = async () => {
|
||||
let relays = await db.getSortedSetMembersWithScores('relays:state');
|
||||
relays = relays.reduce((memo, { value, score }) => {
|
||||
let label = '[[admin/settings/activitypub:relays.state-0]]';
|
||||
switch(score) {
|
||||
case 1: {
|
||||
label = '[[admin/settings/activitypub:relays.state-1]]';
|
||||
break;
|
||||
}
|
||||
|
||||
case 2: {
|
||||
label = '[[admin/settings/activitypub:relays.state-2]]';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
memo.push({
|
||||
url: value,
|
||||
state: score,
|
||||
label,
|
||||
});
|
||||
|
||||
return memo;
|
||||
}, []);
|
||||
|
||||
return relays;
|
||||
};
|
||||
|
||||
Relays.add = async (url) => {
|
||||
const now = Date.now();
|
||||
await activitypub.send('uid', 0, url, {
|
||||
'@context': [
|
||||
'https://www.w3.org/ns/activitystreams',
|
||||
'https://pleroma.example/schemas/litepub-0.1.jsonld',
|
||||
],
|
||||
id: `${nconf.get('url')}/actor#activity/follow/${encodeURIComponent(url)}/${now}`,
|
||||
type: 'Follow',
|
||||
to: [url],
|
||||
object: url,
|
||||
state: 'pending',
|
||||
});
|
||||
|
||||
await Promise.all([
|
||||
db.sortedSetAdd('relays:createtime', now, url),
|
||||
db.sortedSetAdd('relays:state', 0, url),
|
||||
]);
|
||||
};
|
||||
|
||||
Relays.remove = async (url) => {
|
||||
const now = new Date();
|
||||
const createtime = await db.sortedSetScore('relays:createtime', url);
|
||||
if (!createtime) {
|
||||
throw new Error('[[error:invalid-data]]');
|
||||
}
|
||||
|
||||
await activitypub.send('uid', 0, url, {
|
||||
'@context': [
|
||||
'https://www.w3.org/ns/activitystreams',
|
||||
'https://pleroma.example/schemas/litepub-0.1.jsonld',
|
||||
],
|
||||
id: `${nconf.get('url')}/actor#activity/undo:follow/${encodeURIComponent(url)}/${now.getTime()}`,
|
||||
type: 'Undo',
|
||||
to: [url],
|
||||
published: now.toISOString(),
|
||||
object: {
|
||||
'@context': [
|
||||
'https://www.w3.org/ns/activitystreams',
|
||||
'https://pleroma.example/schemas/litepub-0.1.jsonld',
|
||||
],
|
||||
id: `${nconf.get('url')}/actor#activity/follow/${encodeURIComponent(url)}/${createtime}`,
|
||||
type: 'Follow',
|
||||
actor: `${nconf.get('url')}/actor`,
|
||||
to: [url],
|
||||
object: url,
|
||||
state: 'cancelled',
|
||||
},
|
||||
});
|
||||
|
||||
await Promise.all([
|
||||
db.sortedSetRemove('relays:createtime', url),
|
||||
db.sortedSetRemove('relays:state', url),
|
||||
]);
|
||||
};
|
||||
|
||||
Relays.handshake = async (object) => {
|
||||
const now = new Date();
|
||||
const { type, actor } = object;
|
||||
|
||||
// Confirm relay was added
|
||||
const exists = await db.isSortedSetMember('relays:createtime', actor);
|
||||
if (!exists) {
|
||||
throw new Error('[[error:api.400]]');
|
||||
}
|
||||
|
||||
if (type === 'Follow') {
|
||||
await db.sortedSetIncrBy('relays:state', 1, actor);
|
||||
await activitypub.send('uid', 0, actor, {
|
||||
'@context': [
|
||||
'https://www.w3.org/ns/activitystreams',
|
||||
'https://pleroma.example/schemas/litepub-0.1.jsonld',
|
||||
],
|
||||
id: `${nconf.get('url')}/actor#activity/accept/${encodeURIComponent(actor)}/${now.getTime()}`,
|
||||
type: 'Accept',
|
||||
to: [actor],
|
||||
published: now.toISOString(),
|
||||
object,
|
||||
});
|
||||
} else if (type === 'Accept') {
|
||||
await db.sortedSetIncrBy('relays:state', 1, actor);
|
||||
} else {
|
||||
throw new Error('[[error:api.400]]');
|
||||
}
|
||||
};
|
||||
@@ -159,15 +159,17 @@ settingsController.api = async (req, res) => {
|
||||
};
|
||||
|
||||
settingsController.activitypub = async (req, res) => {
|
||||
const [instanceCount, rules] = await Promise.all([
|
||||
const [instanceCount, rules, relays] = await Promise.all([
|
||||
activitypub.instances.getCount(),
|
||||
activitypub.rules.list(),
|
||||
activitypub.relays.list(),
|
||||
]);
|
||||
|
||||
res.render('admin/settings/activitypub', {
|
||||
title: `[[admin/menu:settings/activitypub]]`,
|
||||
instanceCount,
|
||||
rules,
|
||||
relays,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -91,7 +91,7 @@ Admin.activitypub.addRule = async (req, res) => {
|
||||
const { type, value, cid } = req.body;
|
||||
const exists = await categories.exists(cid);
|
||||
if (!value || !exists) {
|
||||
helpers.formatApiResponse(400, res);
|
||||
return helpers.formatApiResponse(400, res);
|
||||
}
|
||||
|
||||
await activitypub.rules.add(type, value, cid);
|
||||
@@ -103,3 +103,17 @@ Admin.activitypub.deleteRule = async (req, res) => {
|
||||
await activitypub.rules.delete(rid);
|
||||
helpers.formatApiResponse(200, res, await activitypub.rules.list());
|
||||
};
|
||||
|
||||
Admin.activitypub.addRelay = async (req, res) => {
|
||||
const { url } = req.body;
|
||||
|
||||
await activitypub.relays.add(url);
|
||||
helpers.formatApiResponse(200, res, await activitypub.relays.list());
|
||||
};
|
||||
|
||||
Admin.activitypub.removeRelay = async (req, res) => {
|
||||
const { url } = req.params;
|
||||
|
||||
await activitypub.relays.remove(url);
|
||||
helpers.formatApiResponse(200, res, await activitypub.relays.list());
|
||||
};
|
||||
|
||||
@@ -27,6 +27,8 @@ module.exports = function () {
|
||||
|
||||
setupApiRoute(router, 'post', '/activitypub/rules', [...middlewares, middleware.checkRequired.bind(null, ['cid', 'value', 'type'])], controllers.write.admin.activitypub.addRule);
|
||||
setupApiRoute(router, 'delete', '/activitypub/rules/:rid', [...middlewares], controllers.write.admin.activitypub.deleteRule);
|
||||
setupApiRoute(router, 'post', '/activitypub/relays', [...middlewares, middleware.checkRequired.bind(null, ['url'])], controllers.write.admin.activitypub.addRelay);
|
||||
setupApiRoute(router, 'delete', '/activitypub/relays/:url', [...middlewares], controllers.write.admin.activitypub.removeRelay);
|
||||
|
||||
return router;
|
||||
};
|
||||
|
||||
11
src/views/admin/partials/activitypub/relays.tpl
Normal file
11
src/views/admin/partials/activitypub/relays.tpl
Normal file
@@ -0,0 +1,11 @@
|
||||
<p>[[admin/settings/activitypub:relays.warning]]</p>
|
||||
<p>[[admin/settings/activitypub:relays.litepub]]</p>
|
||||
|
||||
<hr />
|
||||
|
||||
<form role="form">
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="url">Relay URL</label>
|
||||
<input type="text" id="url" name="url" title="Relay URL" class="form-control" placeholder="https://example.org/actor">
|
||||
</div>
|
||||
</form>
|
||||
@@ -78,6 +78,39 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row settings m-0">
|
||||
<div class="col-sm-2 col-12 settings-header">[[admin/settings/activitypub:relays]]</div>
|
||||
<div class="col-sm-10 col-12">
|
||||
<p>[[admin/settings/activitypub:relays.intro]]</p>
|
||||
<p class="text-warning">[[admin/settings/activitypub:relays.warning]]</p>
|
||||
<div class="mb-3">
|
||||
<table class="table table-striped" id="relays">
|
||||
<thead>
|
||||
<th>[[admin/settings/activitypub:relays.relay]]</th>
|
||||
<th>[[admin/settings/activitypub:relays.state]]</th>
|
||||
<th></th>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{{ each relays }}}
|
||||
<tr data-url="{./url}">
|
||||
<td>{./url}</td>
|
||||
<td>{./label}</td>
|
||||
<td><a href="#" data-action="relays.remove"><i class="fa fa-trash link-danger"></i></a></td>
|
||||
</tr>
|
||||
{{{ end }}}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colspan="3">
|
||||
<button class="btn btn-sm btn-primary" data-action="relays.add">[[admin/settings/activitypub:relays.add]]</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row settings m-0">
|
||||
<div class="col-sm-2 col-12 settings-header">[[admin/settings/activitypub:pruning]]</div>
|
||||
<div class="col-sm-10 col-12">
|
||||
|
||||
Reference in New Issue
Block a user