feat: adding and removing relays from AP settings page in ACP

This commit is contained in:
Julian Lam
2025-08-25 16:50:18 -04:00
parent 6d856545ec
commit 1e0fb20db4
10 changed files with 230 additions and 6 deletions

View File

@@ -28,6 +28,16 @@
"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-pending": "Pending",
"relays.state-active": "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.",

View File

@@ -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 Benchpress.render('admin/settings/activitypub', { relays: data }, 'relays');
const tbodyEl = document.querySelector('#relays tbody');
if (tbodyEl) {
tbodyEl.innerHTML = 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]]',
@@ -75,5 +103,33 @@ 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 Benchpress.render('admin/settings/activitypub', { relays: data }, 'relays');
const tbodyEl = document.querySelector('#relays tbody');
if (tbodyEl) {
tbodyEl.innerHTML = 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,
},
},
});
});
};
return Module;
});

View File

@@ -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) => {

View File

@@ -66,6 +66,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.');

95
src/activitypub/relays.js Normal file
View File

@@ -0,0 +1,95 @@
'use strict';
const nconf = require('nconf');
const db = require('../database');
const activitypub = module.parent.exports;
const Relays = module.exports;
Relays.list = async () => {
let relays = await db.getSortedSetMembersWithScores('relays:state');
relays = relays.reduce((memo, { value, score }) => {
let state = 'Pending';
switch(score) {
case 1: {
state = 'Establishing';
break;
}
case 2: {
state = 'Active';
break;
}
}
memo.push({
url: value,
state,
});
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 (activity) => {
console.log(activity);
};

View File

@@ -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,
});
};

View File

@@ -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());
};

View File

@@ -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;
};

View 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>

View File

@@ -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>{./state}</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">