Files
Haven/test/ferryAutomod.test.js
ancsemi cb77c2b70c Auto-mod allowlist: haven-app.com is on it, so links to Haven's own guide are not blocked
New servers get it with the starter list. Existing servers get it once on
update; an entry the admin already has for it (allowed or blocked) is left as
it is, and removing it afterwards sticks.
2026-10-02 11:06:00 -04:00

138 lines
5.1 KiB
JavaScript

'use strict';
// Ferry relays Discord attachments as cdn.discordapp.com links that it builds
// itself out of Discord's API response. Those hosts are not on Haven's default
// automod allowlist, so when the whole relayed string was fed to the link
// filter, a stock server threw away every Discord message carrying an image,
// the author's text along with it. Only the authored half is filtered now.
//
// The main ferry test file exercises buildHavenContent with automod effectively
// off, because there is no database behind it in a unit test and the module
// short-circuits to { ok: true }. That is why the bug got through, so this file
// loads ferry with a stubbed automod that is switched ON and carrying the real
// seeded allowlist from src/database.js.
const assert = require('node:assert/strict');
const test = require('node:test');
const path = require('node:path');
const Module = require('node:module');
const rules = require('../public/js/automod-rules.js');
// The starter allowlist seeded into every new server, copied from src/database.js.
const SEEDED = [
'youtube.com', 'youtu.be', 'twitch.tv', 'x.com', 'twitter.com', 'bsky.app',
'reddit.com', 'github.com', 'gitlab.com', 'stackoverflow.com', 'wikipedia.org',
'imgur.com', 'giphy.com', 'tenor.com', 'spotify.com', 'soundcloud.com',
'steamcommunity.com', 'steampowered.com', 'last.fm', 'archive.org',
'haven-app.com',
];
const POLICY = {
mode: 'allowlist',
allow: SEEDED.map(domain => ({ domain, includeSubdomains: true })),
deny: [],
blockIpUrls: true,
blockPunycode: true,
blockObfuscated: true,
};
// Stand in for src/automod.js before ferry pulls it in, so this file measures
// the filtering decision rather than the database.
const automodPath = require.resolve('../src/automod.js');
require.cache[automodPath] = {
id: automodPath,
filename: automodPath,
loaded: true,
paths: Module._nodeModulePaths(path.dirname(automodPath)),
exports: {
checkText(text) {
const hit = rules.checkText(text, POLICY);
return hit ? { ok: false, rule: hit.rule, host: hit.host } : { ok: true };
},
},
};
const { buildHavenContent } = require('../src/ferry');
const ATTACHMENT = 'https://cdn.discordapp.com/attachments/1178/8830/photo.png?ex=68b0&is=68af&hm=abcd&';
test('the allowlist really does reject Discord CDN links', () => {
// If this ever stops being true the rest of the file proves nothing.
assert.ok(rules.checkText(ATTACHMENT, POLICY), 'expected the stock allowlist to block cdn.discordapp.com');
});
test('a Discord image survives automod and keeps the text next to it', () => {
const out = buildHavenContent({
content: 'look at this',
attachments: [{ url: ATTACHMENT }],
});
assert.equal(out, `look at this\n${ATTACHMENT}`);
});
test('an image-only Discord message is not silently dropped', () => {
assert.equal(buildHavenContent({ content: '', attachments: [{ url: ATTACHMENT }] }), ATTACHMENT);
});
test('Discord stickers relay too', () => {
const out = buildHavenContent({ content: '', sticker_items: [{ id: '123' }] });
assert.equal(out, 'https://media.discordapp.net/stickers/123.png');
});
test('a blocked link the Discord user typed is still filtered out', () => {
// The protection this whole check exists for has to keep working: a domain
// somebody typed is not the same as a URL Ferry built.
assert.equal(buildHavenContent({ content: 'free stuff https://totally-not-spam.example/x' }), '');
// And it stays blocked even when an attachment rides along with it.
assert.equal(
buildHavenContent({
content: 'free stuff https://totally-not-spam.example/x',
attachments: [{ url: ATTACHMENT }],
}),
''
);
});
test('an allowed domain the user typed still passes', () => {
assert.equal(
buildHavenContent({ content: 'clip https://www.youtube.com/watch?v=abc' }),
'clip https://www.youtube.com/watch?v=abc'
);
});
test('a promoted stream link is kept when the allowlist permits it', () => {
const out = buildHavenContent({
content: '@everyone Streamer is live!',
mention_everyone: true,
embeds: [{
type: 'rich',
title: 'Streamer on Twitch',
url: 'https://www.twitch.tv/streamer',
image: { url: 'https://static-cdn.jtvnw.net/previews-ttv/live_user_streamer-1280x720.jpg' },
}],
});
assert.equal(
out,
'@everyone Streamer is live!\nStreamer on Twitch https://www.twitch.tv/streamer\nhttps://static-cdn.jtvnw.net/previews-ttv/live_user_streamer-1280x720.jpg'
);
});
test('a promoted link off the allowlist drops just the link, not the announcement', () => {
// kick.com is not in the starter allowlist. The old path would have fed the
// link through with the text and lost the whole message.
const out = buildHavenContent({
content: '@everyone Streamer is live on Kick!',
mention_everyone: true,
embeds: [{
type: 'rich',
title: 'Streamer on Kick',
url: 'https://kick.com/streamer',
image: { url: 'https://images.kick.com/video_thumbnails/streamer/720.webp' },
}],
});
assert.equal(
out,
'@everyone Streamer is live on Kick!\nStreamer on Kick\nhttps://images.kick.com/video_thumbnails/streamer/720.webp'
);
});