mirror of
https://github.com/NodeBB/NodeBB.git
synced 2026-09-02 03:06:38 +02:00
feat: Third-party blocklists (#14115)
* feat: third-party blocklists * docs: openapi routes for blocklist routes * fix: handle null blocklists check
This commit is contained in:
@@ -65,6 +65,7 @@
|
||||
"cronstrue": "3.14.0",
|
||||
"cropperjs": "1.6.2",
|
||||
"csrf-sync": "4.2.1",
|
||||
"csv-parse": "6.2.1",
|
||||
"daemon": "1.1.0",
|
||||
"diff": "8.0.4",
|
||||
"esbuild": "0.27.4",
|
||||
|
||||
@@ -41,6 +41,17 @@
|
||||
"relays.state-2": "Active",
|
||||
"relays.errors.invalid-url": "Please enter a valid URL",
|
||||
|
||||
"blocklists": "Third-party Blocklists",
|
||||
"blocklists-help": "For the safety of you and your users, maintaining a blocklist is an essential part of any trust & safety effort when working with content from outside the local moderation scope. NodeBB ships with some recommended defaults, and these can be customized here.",
|
||||
"blocklists-default": "NodeBB ships with two blocklists by default, the <a href=\"https://about.iftas.org/library/iftas-dni-list/\">IFTAS Do Not Interact Denylist</a> and the <a href=\"https://about.iftas.org/library/iftas-abandoned-and-unmanaged-domain-list/\">IFTAS Abandoned and Unmanaged Domain Denylist</a>.",
|
||||
"blocklists.url": "URL",
|
||||
"blocklists.count": "Domains",
|
||||
"blocklists.add": "Add New Blocklist",
|
||||
"blocklists.add-help": "Enter the URL of the blocklist you would like to add. NodeBB understands blocklists that match the format used by <a href=\"https://iftas.org\">IFTAS</a>.",
|
||||
"blocklists.refreshed": "Blocklist refreshed — it now contains %1 entries",
|
||||
"blocklists.view.title": "View Blocklist",
|
||||
"blocklists.view.intro": "These are the %1 domain(s) blocked by this blocklist:",
|
||||
|
||||
"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.",
|
||||
|
||||
14
public/openapi/components/schemas/admin/blocklists.yaml
Normal file
14
public/openapi/components/schemas/admin/blocklists.yaml
Normal file
@@ -0,0 +1,14 @@
|
||||
BlocklistObject:
|
||||
type: object
|
||||
properties:
|
||||
url:
|
||||
type: string
|
||||
description: The blocklist URL
|
||||
example: https://example.org/blocklist
|
||||
count:
|
||||
type: number
|
||||
description: The number of blocked domains on the list
|
||||
BlocklistArray:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/BlocklistObject'
|
||||
@@ -13,6 +13,8 @@ get:
|
||||
properties:
|
||||
title:
|
||||
type: string
|
||||
blocklists:
|
||||
$ref: ../../../components/schemas/admin/blocklists.yaml#/BlocklistArray
|
||||
instanceCount:
|
||||
type: number
|
||||
- $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps
|
||||
@@ -286,6 +286,12 @@ paths:
|
||||
$ref: 'write/admin/activitypub/relays.yaml'
|
||||
/admin/activitypub/relays/{url}:
|
||||
$ref: 'write/admin/activitypub/relays/url.yaml'
|
||||
/admin/activitypub/blocklists:
|
||||
$ref: 'write/admin/activitypub/blocklists.yaml'
|
||||
/admin/activitypub/blocklists/{url}:
|
||||
$ref: 'write/admin/activitypub/blocklists/url.yaml'
|
||||
/admin/activitypub/blocklists/{url}/refresh:
|
||||
$ref: 'write/admin/activitypub/blocklists/url/refresh.yaml'
|
||||
/files/:
|
||||
$ref: 'write/files.yaml'
|
||||
/files/folder:
|
||||
|
||||
28
public/openapi/write/admin/activitypub/blocklists.yaml
Normal file
28
public/openapi/write/admin/activitypub/blocklists.yaml
Normal file
@@ -0,0 +1,28 @@
|
||||
post:
|
||||
tags:
|
||||
- admin
|
||||
summary: add blocklist
|
||||
description: This operation add a new blocklist
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
url:
|
||||
type: string
|
||||
description: The blocklist url
|
||||
example: https://example.org/blocklist
|
||||
responses:
|
||||
'200':
|
||||
description: blocklist successfully added
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
$ref: ../../../components/schemas/Status.yaml#/Status
|
||||
response:
|
||||
$ref: ../../../components/schemas/admin/blocklists.yaml#/BlocklistArray
|
||||
55
public/openapi/write/admin/activitypub/blocklists/url.yaml
Normal file
55
public/openapi/write/admin/activitypub/blocklists/url.yaml
Normal file
@@ -0,0 +1,55 @@
|
||||
get:
|
||||
tags:
|
||||
- admin
|
||||
summary: view blocklist
|
||||
description: This operation retrieves the domains in a specific blocklist
|
||||
parameters:
|
||||
- name: url
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
example: https%3A%2F%2Fexample.org%2Fblocklist
|
||||
responses:
|
||||
'200':
|
||||
description: blocklist successfully retrieved
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
$ref: ../../../../components/schemas/Status.yaml#/Status
|
||||
response:
|
||||
type: object
|
||||
properties:
|
||||
domains:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
count:
|
||||
type: number
|
||||
delete:
|
||||
tags:
|
||||
- admin
|
||||
summary: delete blocklist
|
||||
description: This operation removes a previously-added blocklist
|
||||
parameters:
|
||||
- in: path
|
||||
name: url
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
example: https%3A%2F%2Fexample.org%2Fblocklist
|
||||
responses:
|
||||
'200':
|
||||
description: blocklist successfully deleted
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
$ref: ../../../../components/schemas/Status.yaml#/Status
|
||||
response:
|
||||
$ref: ../../../../components/schemas/admin/blocklists.yaml#/BlocklistArray
|
||||
@@ -0,0 +1,30 @@
|
||||
post:
|
||||
tags:
|
||||
- admin
|
||||
summary: refresh blocklist
|
||||
description: This operation refreshes the blocklist domains
|
||||
parameters:
|
||||
- name: url
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
example: https%3A%2F%2Fexample.org%2Fblocklist
|
||||
responses:
|
||||
'200':
|
||||
description: blocklist successfully refreshed
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
$ref: ../../../../../components/schemas/Status.yaml#/Status
|
||||
response:
|
||||
type: object
|
||||
properties:
|
||||
blocklists:
|
||||
$ref: ../../../../../components/schemas/admin/blocklists.yaml#/BlocklistArray
|
||||
count:
|
||||
type: number
|
||||
description: The number of domains managed by this blocklist
|
||||
113
public/src/admin/federation/safety.js
Normal file
113
public/src/admin/federation/safety.js
Normal file
@@ -0,0 +1,113 @@
|
||||
'use strict';
|
||||
|
||||
import { get, post, del } from 'api';
|
||||
import { error } from 'alerts';
|
||||
import { render } from 'benchpress';
|
||||
import { alert, dialog } from 'bootbox';
|
||||
|
||||
export function init() {
|
||||
setupBlocklists();
|
||||
};
|
||||
|
||||
function setupBlocklists() {
|
||||
const blocklistsEl = document.getElementById('blocklists');
|
||||
if (blocklistsEl) {
|
||||
blocklistsEl.addEventListener('click', (e) => {
|
||||
const subselector = e.target.closest('[data-action]');
|
||||
if (subselector) {
|
||||
const url = subselector.closest('tr').getAttribute('data-url');
|
||||
const action = subselector.getAttribute('data-action');
|
||||
switch (action) {
|
||||
case 'blocklists.add': {
|
||||
throwModal();
|
||||
break;
|
||||
}
|
||||
|
||||
case 'blocklists.refresh': {
|
||||
refresh(url);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'blocklists.view': {
|
||||
view(url);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'blocklists.remove': {
|
||||
del(`/admin/activitypub/blocklists/${encodeURIComponent(url)}`, {})
|
||||
.then(renderList)
|
||||
.catch(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function renderList(blocklists) {
|
||||
const html = await app.parseAndTranslate('admin/federation/safety', 'blocklists', { blocklists });
|
||||
const tbodyEl = document.querySelector('#blocklists tbody');
|
||||
if (tbodyEl) {
|
||||
$(tbodyEl).html(html);
|
||||
}
|
||||
}
|
||||
|
||||
function throwModal() {
|
||||
render('admin/partials/activitypub/blocklists', {}).then(function (html) {
|
||||
const submit = function () {
|
||||
const formEl = modal.find('form').get(0);
|
||||
if (!formEl.reportValidity()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const payload = Object.fromEntries(new FormData(formEl));
|
||||
post('/admin/activitypub/blocklists', payload).then(async (data) => {
|
||||
const html = await app.parseAndTranslate('admin/federation/safety', 'blocklists', { blocklists: data });
|
||||
const tbodyEl = document.querySelector('#blocklists tbody');
|
||||
if (tbodyEl) {
|
||||
$(tbodyEl).html(html);
|
||||
}
|
||||
}).catch(error);
|
||||
};
|
||||
const modal = bootbox.dialog({
|
||||
title: '[[admin/settings/activitypub:blocklists.add]]',
|
||||
message: html,
|
||||
buttons: {
|
||||
save: {
|
||||
label: '[[global:save]]',
|
||||
className: 'btn-primary',
|
||||
callback: submit,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
modal.on('shown.bs.modal', function () {
|
||||
modal.find('input').focus();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function refresh(url) {
|
||||
try {
|
||||
const data = await post(`/admin/activitypub/blocklists/${encodeURIComponent(url)}/refresh`);
|
||||
alert(`[[admin/settings/activitypub:blocklists.refreshed, ${data.count}]]`);
|
||||
renderList(data.blocklists);
|
||||
} catch (e) {
|
||||
error(e);
|
||||
}
|
||||
}
|
||||
|
||||
async function view(url) {
|
||||
console.log('ehjre');
|
||||
try {
|
||||
const { domains, count } = await get(`/admin/activitypub/blocklists/${encodeURIComponent(url)}`);
|
||||
dialog({
|
||||
title: '[[admin/settings/activitypub:blocklists.view.title]]',
|
||||
message: `\
|
||||
<p>[[admin/settings/activitypub:blocklists.view.intro, ${count}]]</p> \
|
||||
<ul>` + domains.map(domain => `<li>${domain}</li>`).join('\n') + '</ul>',
|
||||
});
|
||||
} catch (e) {
|
||||
error(e);
|
||||
}
|
||||
}
|
||||
78
src/activitypub/blocklists.js
Normal file
78
src/activitypub/blocklists.js
Normal file
@@ -0,0 +1,78 @@
|
||||
'use strict';
|
||||
|
||||
const { parse } = require('csv-parse/sync');
|
||||
|
||||
const db = require('../database');
|
||||
const request = require('../request');
|
||||
const activitypub = module.parent.exports;
|
||||
|
||||
const Blocklists = module.exports;
|
||||
|
||||
Blocklists.list = async () => {
|
||||
const blocklists = await db.getSortedSetMembers('blocklists');
|
||||
const counts = await db.sortedSetsCard(blocklists.map(blocklist => `blocklist:${blocklist}`));
|
||||
|
||||
return blocklists.map((url, idx) => {
|
||||
return { url, count: counts[idx] };
|
||||
});
|
||||
};
|
||||
|
||||
Blocklists.get = async (url) => {
|
||||
const domains = await db.getSortedSetMembers(`blocklist:${url}`);
|
||||
|
||||
return {
|
||||
domains,
|
||||
count: domains.length,
|
||||
};
|
||||
};
|
||||
|
||||
Blocklists.add = async (url) => {
|
||||
const now = Date.now();
|
||||
|
||||
await Promise.all([
|
||||
db.sortedSetAdd('blocklists', now, url),
|
||||
Blocklists.refresh(url),
|
||||
]);
|
||||
};
|
||||
|
||||
Blocklists.remove = async (url) => {
|
||||
await Promise.all([
|
||||
db.sortedSetRemove('blocklists', url),
|
||||
db.delete(`blocklist:${url}`),
|
||||
]);
|
||||
};
|
||||
|
||||
Blocklists.refresh = async (url) => {
|
||||
activitypub.helpers.log(`[blocklists/refresh] Processing ${url}`);
|
||||
|
||||
const { body: csv } = await request.get(url);
|
||||
let records;
|
||||
try {
|
||||
records = parse(csv, {
|
||||
columns: true,
|
||||
skip_empty_lines: true,
|
||||
trim: true,
|
||||
});
|
||||
if (!records.length) {
|
||||
return 0;
|
||||
}
|
||||
} catch (e) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
await db.sortedSetAdd(
|
||||
`blocklist:${url}`,
|
||||
records.map(entry => entry['#severity'] === 'silence' ? 2 : 1),
|
||||
records.map(entry => entry['#domain'])
|
||||
);
|
||||
|
||||
return records.length;
|
||||
};
|
||||
|
||||
Blocklists.check = async (domain) => {
|
||||
const blocklists = await Blocklists.list();
|
||||
let present = await db.isMemberOfSortedSets(blocklists.map(({ url }) => `blocklist:${url}`), domain);
|
||||
present = present.reduce((memo, present) => memo || present, false);
|
||||
|
||||
return !present;
|
||||
};
|
||||
@@ -64,6 +64,7 @@ ActivityPub.notes = require('./notes');
|
||||
ActivityPub.contexts = require('./contexts');
|
||||
ActivityPub.actors = require('./actors');
|
||||
ActivityPub.instances = require('./instances');
|
||||
ActivityPub.blocklists = require('./blocklists');
|
||||
ActivityPub.feps = require('./feps');
|
||||
ActivityPub.rules = require('./rules');
|
||||
ActivityPub.relays = require('./relays');
|
||||
@@ -134,11 +135,11 @@ ActivityPub.resolveInboxes = async (ids) => {
|
||||
|
||||
// Filter out blocked instances
|
||||
const blocked = [];
|
||||
inboxArr = inboxArr.filter((inbox) => {
|
||||
const allowed = await Promise.all(inboxArr.map(async (inbox) => {
|
||||
let allowed = false;
|
||||
try {
|
||||
const { hostname } = new URL(inbox);
|
||||
allowed = ActivityPub.instances.isAllowed(hostname);
|
||||
allowed = await ActivityPub.instances.isAllowed(hostname);
|
||||
if (!allowed) {
|
||||
blocked.push(inbox);
|
||||
}
|
||||
@@ -147,7 +148,8 @@ ActivityPub.resolveInboxes = async (ids) => {
|
||||
}
|
||||
|
||||
return allowed;
|
||||
});
|
||||
}));
|
||||
inboxArr = inboxArr.filter((_, idx) => allowed[idx]);
|
||||
if (blocked.length) {
|
||||
ActivityPub.helpers.log(`[activitypub/resolveInboxes] Not delivering to blocked instances: ${blocked.join(', ')}`);
|
||||
}
|
||||
@@ -306,7 +308,7 @@ ActivityPub.get = async (type, id, uri, options) => {
|
||||
}
|
||||
|
||||
const { hostname } = new URL(uri);
|
||||
const allowed = ActivityPub.instances.isAllowed(hostname);
|
||||
const allowed = await ActivityPub.instances.isAllowed(hostname);
|
||||
if (!allowed) {
|
||||
ActivityPub.helpers.log(`[activitypub/get] Not retrieving ${uri}, domain is blocked.`);
|
||||
const e = new Error(`[[error:activitypub.get-failed]]`);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
const meta = require('../meta');
|
||||
const db = require('../database');
|
||||
const activitypub = module.parent.exports;
|
||||
|
||||
const Instances = module.exports;
|
||||
|
||||
@@ -11,9 +12,15 @@ Instances.log = async (domain) => {
|
||||
|
||||
Instances.getCount = async () => db.sortedSetCard('instances:lastSeen');
|
||||
|
||||
Instances.isAllowed = (domain) => {
|
||||
Instances.isAllowed = async (domain) => {
|
||||
const allowed = await activitypub.blocklists.check(domain);
|
||||
let { activitypubFilter: type, activitypubFilterList: list } = meta.config;
|
||||
|
||||
if (!allowed && !type) {
|
||||
return allowed;
|
||||
}
|
||||
|
||||
list = new Set(String(list).split('\n'));
|
||||
// eslint-disable-next-line no-bitwise
|
||||
return list.has(domain) ^ !type;
|
||||
return allowed || (list.has(domain) ^ !type);
|
||||
};
|
||||
|
||||
@@ -50,6 +50,18 @@ Jobs.start = async () => {
|
||||
runOnInit: false,
|
||||
onTick: async () => await tryCronJob(backfill),
|
||||
});
|
||||
|
||||
await cron.addJob({
|
||||
name: 'ap:blocklist:refresh',
|
||||
cronTime: '15 0 * * *',
|
||||
runOnInit: false,
|
||||
onTick: async () => {
|
||||
const lists = await activitypub.blocklists.list();
|
||||
await Promise.all(lists.map(({ url }) => {
|
||||
return activitypub.blocklists.refresh(url);
|
||||
}));
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
async function retryFailedMessages() {
|
||||
|
||||
@@ -44,9 +44,11 @@ federationController.pruning = function (req, res) {
|
||||
|
||||
federationController.safety = async function (req, res) {
|
||||
const instanceCount = await activitypub.instances.getCount();
|
||||
const blocklists = await activitypub.blocklists.list();
|
||||
|
||||
res.render(`admin/federation/safety`, {
|
||||
title: '[[admin/menu:federation/safety]]',
|
||||
blocklists,
|
||||
instanceCount,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -126,3 +126,36 @@ Admin.activitypub.removeRelay = async (req, res) => {
|
||||
await activitypub.relays.remove(url);
|
||||
helpers.formatApiResponse(200, res, await activitypub.relays.list());
|
||||
};
|
||||
|
||||
|
||||
Admin.activitypub.addBlocklist = async (req, res, next) => {
|
||||
const { url } = req.body;
|
||||
if (!url) {
|
||||
return next();
|
||||
}
|
||||
|
||||
await activitypub.blocklists.add(url);
|
||||
helpers.formatApiResponse(200, res, await activitypub.blocklists.list());
|
||||
};
|
||||
|
||||
Admin.activitypub.viewBlocklist = async (req, res) => {
|
||||
const { url } = req.params;
|
||||
|
||||
helpers.formatApiResponse(200, res, await activitypub.blocklists.get(url));
|
||||
};
|
||||
|
||||
Admin.activitypub.removeBlocklist = async (req, res) => {
|
||||
const { url } = req.params;
|
||||
|
||||
await activitypub.blocklists.remove(url);
|
||||
helpers.formatApiResponse(200, res, await activitypub.blocklists.list());
|
||||
};
|
||||
|
||||
Admin.activitypub.refreshBlocklist = async (req, res) => {
|
||||
const { url } = req.params;
|
||||
|
||||
const count = await activitypub.blocklists.refresh(url);
|
||||
const blocklists = await activitypub.blocklists.list();
|
||||
|
||||
helpers.formatApiResponse(200, res, { blocklists, count });
|
||||
};
|
||||
|
||||
@@ -244,6 +244,12 @@ async function setupDefaultConfigs() {
|
||||
|
||||
await meta.configs.setOnEmpty(defaults);
|
||||
await meta.configs.init();
|
||||
|
||||
const db = require('./database');
|
||||
await db.sortedSetAdd('blocklists', [Date.now(), Date.now()], [
|
||||
'https://about.iftas.org/wp-content/uploads/2025/10/iftas-dni-latest.csv',
|
||||
'https://about.iftas.org/wp-content/uploads/2025/10/iftas-abandoned-unmanaged-latest.csv',
|
||||
]);
|
||||
}
|
||||
|
||||
async function enableDefaultTheme() {
|
||||
|
||||
@@ -105,7 +105,7 @@ middleware.assertPayload = helpers.try(async function (req, res, next) {
|
||||
|
||||
// Domain check
|
||||
const { hostname } = new URL(actor);
|
||||
const allowed = activitypub.instances.isAllowed(hostname);
|
||||
const allowed = await activitypub.instances.isAllowed(hostname);
|
||||
if (!allowed) {
|
||||
activitypub.helpers.log(`[middleware/activitypub] Blocked incoming activity from ${hostname}.`);
|
||||
return res.sendStatus(403);
|
||||
|
||||
@@ -30,6 +30,10 @@ module.exports = function () {
|
||||
setupApiRoute(router, 'put', '/activitypub/rules/order', [...middlewares, middleware.checkRequired.bind(null, ['rids'])], controllers.write.admin.activitypub.reorderRules);
|
||||
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);
|
||||
setupApiRoute(router, 'post', '/activitypub/blocklists', [...middlewares, middleware.checkRequired.bind(null, ['url'])], controllers.write.admin.activitypub.addBlocklist);
|
||||
setupApiRoute(router, 'get', '/activitypub/blocklists/:url', [...middlewares], controllers.write.admin.activitypub.viewBlocklist);
|
||||
setupApiRoute(router, 'delete', '/activitypub/blocklists/:url', [...middlewares], controllers.write.admin.activitypub.removeBlocklist);
|
||||
setupApiRoute(router, 'post', '/activitypub/blocklists/:url/refresh', [...middlewares], controllers.write.admin.activitypub.refreshBlocklist);
|
||||
|
||||
return router;
|
||||
};
|
||||
|
||||
14
src/upgrades/4.11.0/subscribe-to-default-blocklists.js
Normal file
14
src/upgrades/4.11.0/subscribe-to-default-blocklists.js
Normal file
@@ -0,0 +1,14 @@
|
||||
'use strict';
|
||||
|
||||
const activitypub = require('../../activitypub');
|
||||
|
||||
module.exports = {
|
||||
name: 'Subscribe to IFTAS DNI and AUD denylists',
|
||||
timestamp: Date.UTC(2026, 2, 23),
|
||||
method: async () => {
|
||||
await Promise.all([
|
||||
activitypub.blocklists.add('https://about.iftas.org/wp-content/uploads/2025/10/iftas-dni-latest.csv'),
|
||||
activitypub.blocklists.add('https://about.iftas.org/wp-content/uploads/2025/10/iftas-abandoned-unmanaged-latest.csv'),
|
||||
]);
|
||||
},
|
||||
};
|
||||
@@ -3,6 +3,45 @@
|
||||
|
||||
<div class="row settings m-0">
|
||||
<div id="spy-container" class="col-12 col-md-8 px-0 mb-4" tabindex="0">
|
||||
<div id="denylists" class="mb-4">
|
||||
<h5 class="fw-bold tracking-tight settings-header">[[admin/settings/activitypub:blocklists]]</h5>
|
||||
<div class="mb-3">
|
||||
<p>[[admin/settings/activitypub:blocklists-help]]</p>
|
||||
<p class="form-text">[[admin/settings/activitypub:blocklists-default]]</p>
|
||||
<div class="mb-3 table-responsive-md">
|
||||
<table class="table table-striped" id="blocklists">
|
||||
<thead>
|
||||
<th>[[admin/settings/activitypub:blocklists.url]]</th>
|
||||
<th>[[admin/settings/activitypub:blocklists.count]]</th>
|
||||
<th></th>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{{ each blocklists }}}
|
||||
<tr data-url="{./url}">
|
||||
<td>{./url}</td>
|
||||
<td>{./count}</td>
|
||||
<td>
|
||||
<div class="d-flex gap-3">
|
||||
<a href="#" data-action="blocklists.view"><i class="fa fa-list"></i></a>
|
||||
<a href="#" data-action="blocklists.refresh"><i class="fa fa-refresh"></i></a>
|
||||
<a href="#" data-action="blocklists.remove"><i class="fa fa-trash link-danger"></i></a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{{{ end }}}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colspan="3">
|
||||
<button class="btn btn-sm btn-primary" data-action="blocklists.add">[[admin/settings/activitypub:blocklists.add]]</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="server-filtering" class="mb-4">
|
||||
<h5 class="fw-bold tracking-tight settings-header">[[admin/settings/activitypub:server-filtering]]</h5>
|
||||
<form>
|
||||
|
||||
10
src/views/admin/partials/activitypub/blocklists.tpl
Normal file
10
src/views/admin/partials/activitypub/blocklists.tpl
Normal file
@@ -0,0 +1,10 @@
|
||||
<p>[[admin/settings/activitypub:blocklists.add-help]]</p>
|
||||
|
||||
<hr />
|
||||
|
||||
<form role="form">
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="url">Blocklist URL</label>
|
||||
<input type="url" id="url" name="url" title="Blocklist URL" class="form-control" placeholder="https://example.org/blocklist.csv" required>
|
||||
</div>
|
||||
</form>
|
||||
Reference in New Issue
Block a user