From 1e0fb20db4fb2cdbfef958d8c4c568e4886bc4c9 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Mon, 25 Aug 2025 16:50:18 -0400 Subject: [PATCH] feat: adding and removing relays from AP settings page in ACP --- .../en-GB/admin/settings/activitypub.json | 10 ++ public/src/admin/settings/activitypub.js | 62 +++++++++++- src/activitypub/helpers.js | 2 +- src/activitypub/index.js | 1 + src/activitypub/relays.js | 95 +++++++++++++++++++ src/controllers/admin/settings.js | 4 +- src/controllers/write/admin.js | 16 +++- src/routes/write/admin.js | 2 + .../admin/partials/activitypub/relays.tpl | 11 +++ src/views/admin/settings/activitypub.tpl | 33 +++++++ 10 files changed, 230 insertions(+), 6 deletions(-) create mode 100644 src/activitypub/relays.js create mode 100644 src/views/admin/partials/activitypub/relays.tpl diff --git a/public/language/en-GB/admin/settings/activitypub.json b/public/language/en-GB/admin/settings/activitypub.json index 9a11dbe972..6b84d772be 100644 --- a/public/language/en-GB/admin/settings/activitypub.json +++ b/public/language/en-GB/admin/settings/activitypub.json @@ -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 /actor.", + "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 %1 server(s)", "server.filter-help": "Specify servers you would like to bar from federating with your NodeBB. Alternatively, you may opt to selectively allow federation with specific servers, instead. Both options are supported, although they are mutually exclusive.", diff --git a/public/src/admin/settings/activitypub.js b/public/src/admin/settings/activitypub.js index ad112b2398..b405467f77 100644 --- a/public/src/admin/settings/activitypub.js +++ b/public/src/admin/settings/activitypub.js @@ -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; }); diff --git a/src/activitypub/helpers.js b/src/activitypub/helpers.js index cb6772a408..a8c5532980 100644 --- a/src/activitypub/helpers.js +++ b/src/activitypub/helpers.js @@ -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) => { diff --git a/src/activitypub/index.js b/src/activitypub/index.js index f413b9f644..be1dbf8511 100644 --- a/src/activitypub/index.js +++ b/src/activitypub/index.js @@ -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.'); diff --git a/src/activitypub/relays.js b/src/activitypub/relays.js new file mode 100644 index 0000000000..b222f0b5b7 --- /dev/null +++ b/src/activitypub/relays.js @@ -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); +}; \ No newline at end of file diff --git a/src/controllers/admin/settings.js b/src/controllers/admin/settings.js index 762a2d896f..184c6c0ed6 100644 --- a/src/controllers/admin/settings.js +++ b/src/controllers/admin/settings.js @@ -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, }); }; diff --git a/src/controllers/write/admin.js b/src/controllers/write/admin.js index 132462faaf..8fc7151dc0 100644 --- a/src/controllers/write/admin.js +++ b/src/controllers/write/admin.js @@ -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()); +}; diff --git a/src/routes/write/admin.js b/src/routes/write/admin.js index d5b0fcad9c..050e1ecf7a 100644 --- a/src/routes/write/admin.js +++ b/src/routes/write/admin.js @@ -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; }; diff --git a/src/views/admin/partials/activitypub/relays.tpl b/src/views/admin/partials/activitypub/relays.tpl new file mode 100644 index 0000000000..8f53b167f0 --- /dev/null +++ b/src/views/admin/partials/activitypub/relays.tpl @@ -0,0 +1,11 @@ +

[[admin/settings/activitypub:relays.warning]]

+

[[admin/settings/activitypub:relays.litepub]]

+ +
+ +
+
+ + +
+
\ No newline at end of file diff --git a/src/views/admin/settings/activitypub.tpl b/src/views/admin/settings/activitypub.tpl index 7a4f1e8430..5fa72463a7 100644 --- a/src/views/admin/settings/activitypub.tpl +++ b/src/views/admin/settings/activitypub.tpl @@ -78,6 +78,39 @@ +
+
[[admin/settings/activitypub:relays]]
+
+

[[admin/settings/activitypub:relays.intro]]

+

[[admin/settings/activitypub:relays.warning]]

+
+ + + + + + + + {{{ each relays }}} + + + + + + {{{ end }}} + + + + + + +
[[admin/settings/activitypub:relays.relay]][[admin/settings/activitypub:relays.state]]
{./url}{./state}
+ +
+
+
+
+
[[admin/settings/activitypub:pruning]]