fix: allow break string and summary limits to be defined and applied

This commit is contained in:
Julian Lam
2026-02-26 15:43:02 -05:00
parent 50f5541ef7
commit 29111ba7ca
10 changed files with 99 additions and 13 deletions

View File

@@ -206,5 +206,7 @@
"activitypubProbeTimeout": 2000,
"activitypubContentPruneDays": 30,
"activitypubUserPruneDays": 7,
"activitypubFilter": 0
"activitypubFilter": 0,
"activitypubSummaryLimit": 500,
"activitypubBreakString": "[...]"
}

View File

@@ -50,9 +50,10 @@
"section-federation": "Federation",
"federation/general": "General",
"federation/content": "Content",
"federation/rules": "Categorization",
"federation/relays": "Relays",
"federation/pruning": "Content Pruning",
"federation/pruning": "Storage",
"federation/safety": "Trust & Safety",
"section-appearance": "Appearance",

View File

@@ -44,5 +44,11 @@
"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.",
"server.filter-help-hostname": "Enter just the instance hostname below (e.g. <code>example.org</code>), separated by line breaks.",
"server.filter-allow-list": "Use this as an Allow List instead"
"server.filter-allow-list": "Use this as an Allow List instead",
"content.outgoing": "Outgoing",
"content.summary-limit": "Character count after which a summary is generated",
"content.summary-limit-help": "When content is federated out that exceeds this character count, a <code>summary</code> is generated, comprising of all complete sentences prior to this limit. (Default: 500)",
"content.break-string": "Note/Article Delimiter",
"content.break-string-help": "This delimiter can be manually inserted by power users when composing new topics. It instructs NodeBB to use content up until that point as part of the <code>summary</code>. If this string is not used, then the character count fallback applies. (Default: <code>[...]</code>)"
}

View File

@@ -8,6 +8,7 @@ const sanitize = require('sanitize-html');
const tokenizer = require('sbd');
const db = require('../database');
const meta = require('../meta');
const user = require('../user');
const categories = require('../categories');
const posts = require('../posts');
@@ -710,7 +711,7 @@ Mocks.notes.public = async (post) => {
// Special handling for main posts (as:Article w/ as:Note preview)
const plaintext = posts.sanitizePlaintext(content);
const isArticle = post.pid === post.topic.mainPid && plaintext.length > 500;
const isArticle = post.pid === post.topic.mainPid && plaintext.length > meta.config.activitypubSummaryLimit;
if (post.isMainPost) {
const thumbs = await topics.thumbs.get(post.tid);
@@ -755,17 +756,16 @@ Mocks.notes.public = async (post) => {
// attachment,
// };
const breakString = '[...]';
if (post.content.includes(breakString)) {
const index = post.content.indexOf(breakString);
summary = post.content.slice(0, index + breakString.length);
if (post.content.includes(meta.config.activitypubBreakString)) {
const index = post.content.indexOf(meta.config.activitypubBreakString);
summary = post.content.slice(0, index + meta.config.activitypubBreakString.length);
} else {
const sentences = tokenizer.sentences(post.content, { newline_boundaries: true });
// Append sentences to summary until it contains just under 500 characters of content
const limit = 500;
// Append sentences to summary until until just under configured character limit
const limit = meta.config.activitypubSummaryLimit;
let remaining = limit;
let finished = false;
summary = sentences.reduce((memo, sentence) => {
summary = sentences.reduce((memo, sentence, index) => {
if (finished) {
return memo;
}
@@ -776,7 +776,7 @@ Mocks.notes.public = async (post) => {
});
remaining = remaining - clean.length;
if (remaining > 0) {
memo += ` ${sentence}`;
memo += `${index > 0 ? ' ' : ''}${sentence}`;
} else { // There was more but summary generation is complete
finished = true;
memo += ' [...]';

View File

@@ -10,6 +10,12 @@ federationController.general = function (req, res) {
});
};
federationController.content = function (req, res) {
res.render(`admin/federation/content`, {
title: '[[admin/menu:federation/content]]',
});
};
federationController.rules = async function (req, res) {
const rules = await activitypub.rules.list();

View File

@@ -58,7 +58,7 @@ module.exports = function (Posts) {
}
if (!type.startsWith('activitypub.')) {
postData.content = postData.content.replace('[...]', '');
postData.content = postData.content.replace(meta.config.activitypubBreakString, '');
}
({ postData } = await plugins.hooks.fire('filter:parse.post', { postData, type }));
postData.content = translator.escape(postData.content);

View File

@@ -53,6 +53,7 @@ module.exports = function (app, name, middleware, controllers) {
helpers.setupAdminPageRoute(app, `/${name}/settings/advanced`, middlewares, controllers.admin.settings.advanced);
helpers.setupAdminPageRoute(app, `/${name}/federation/general`, middlewares, controllers.admin.federation.general);
helpers.setupAdminPageRoute(app, `/${name}/federation/content`, middlewares, controllers.admin.federation.content);
helpers.setupAdminPageRoute(app, `/${name}/federation/rules`, middlewares, controllers.admin.federation.rules);
helpers.setupAdminPageRoute(app, `/${name}/federation/relays`, middlewares, controllers.admin.federation.relays);
helpers.setupAdminPageRoute(app, `/${name}/federation/pruning`, middlewares, controllers.admin.federation.pruning);

View File

@@ -0,0 +1,29 @@
<div class="acp-page-container">
<!-- IMPORT admin/partials/settings/header.tpl -->
<div class="row settings m-0">
<div id="spy-container" class="col-12 col-md-8 px-0 mb-4" tabindex="0">
<div id="outgoing" class="mb-4">
<h5 class="fw-bold tracking-tight settings-header">[[admin/settings/activitypub:content.outgoing]]</h5>
<form>
<div class="mb-3">
<label class="form-label" for="activitypubSummaryLimit">[[admin/settings/activitypub:content.summary-limit]]</label>
<input type="number" id="activitypubSummaryLimit" name="activitypubSummaryLimit" data-field="activitypubSummaryLimit" title="[[admin/settings/activitypub:content.summary-limit]]" class="form-control" />
<div class="form-text">
[[admin/settings/activitypub:content.summary-limit-help]]
</div>
</div>
<div class="mb-3">
<label class="form-label" for="activitypubBreakString">[[admin/settings/activitypub:content.break-string]]</label>
<input type="text" id="activitypubBreakString" name="activitypubBreakString" data-field="activitypubBreakString" title="[[admin/settings/activitypub:content.break-string]]" class="form-control" />
<div class="form-text">
[[admin/settings/activitypub:content.break-string-help]]
</div>
</div>
</form>
</div>
</div>
<!-- IMPORT admin/partials/settings/toc.tpl -->
</div>
</div>

View File

@@ -104,6 +104,7 @@
<div id="collapseFederation" class="accordion-collapse collapse" data-bs-parent="#accordionACP">
<div class="accordion-body p-0 d-grid">
<a class="btn btn-ghost btn-sm text-start" id="federation-general" href="{relative_path}/admin/federation/general">[[admin/menu:federation/general]]</a>
<a class="btn btn-ghost btn-sm text-start" id="federation-content" href="{relative_path}/admin/federation/content">[[admin/menu:federation/content]]</a>
<a class="btn btn-ghost btn-sm text-start" id="federation-rules" href="{relative_path}/admin/federation/rules">[[admin/menu:federation/rules]]</a>
<a class="btn btn-ghost btn-sm text-start" id="federation-relays" href="{relative_path}/admin/federation/relays">[[admin/menu:federation/relays]]</a>
<a class="btn btn-ghost btn-sm text-start" id="federation-pruning" href="{relative_path}/admin/federation/pruning">[[admin/menu:federation/pruning]]</a>

View File

@@ -4,6 +4,7 @@ const assert = require('assert');
const nconf = require('nconf');
const db = require('../mocks/databasemock');
const meta = require('../../src/meta');
const user = require('../../src/user');
const categories = require('../../src/categories');
const topics = require('../../src/topics');
@@ -82,6 +83,45 @@ describe('Mocking', () => {
const { content } = await posts.parsePost(clone);
assert(!content.includes('[...]'));
});
describe('Altered magic break string', () => {
let string;
before(() => {
string = meta.config.activitypubBreakString;
meta.config.activitypubBreakString = 'Mauris';
});
after(() => {
meta.config.activitypubBreakString = string;
});
it('should work with a customized break string', async function () {
const mocked = await activitypub.mocks.notes.public(this.withBreakPost);
assert.strictEqual(mocked.summary, 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.\
Aliquam vel augue, id luctus nulla. Mauris');
});
});
describe('Altered summary limit', () => {
let string;
let limit;
before(() => {
string = meta.config.activitypubBreakString;
meta.config.activitypubBreakString = 'lkjsdnfkjsdfkjsdhfkd';
limit = meta.config.activitypubSummaryLimit;
meta.config.activitypubSummaryLimit = 60;
});
after(() => {
meta.config.activitypubBreakString = string;
meta.config.activitypubSummaryLimit = limit;
});
it('should work with a customized summary limit', async function () {
const mocked = await activitypub.mocks.notes.public(this.withBreakPost);
assert.strictEqual(mocked.summary, 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. [...]');
});
});
});
});
});