mirror of
https://github.com/ancsemi/Haven.git
synced 2026-10-28 01:26:14 +00:00
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.
138 lines
5.1 KiB
JavaScript
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'
|
|
);
|
|
});
|