source: Klonkt/test/inbound-mentions.test.js@ 9622f20

main
Last change on this file since 9622f20 was fe97cc3, checked in by roboburr <roboburr@…>, 2 months ago

feat(fediverse): notification when mentioned in a non-reply post

An inbound Create whose Mention tag targets one of our actors — but which isn't a
reply to our content — was silently ignored. It's now stored and shown in the
fediverse notifications (@ icon, snippet, link to the original note). Only hrefs on
our own base count (a /ap/users/<slug> path on a remote host is someone else's actor).

  • src/config/database.js — ap_mentions table (per-site, UNIQUE(slug, object_uri)).
  • src/services/ActivityPubService.js — localMentionSlugs(tags, base) detection helper (own-base guard, slug must exist, deduped); the Create branch stores a mention per targeted site; getNotifications includes mentions.
  • src/views/pages/fedi-notifications.ejs — mention item (@ icon, content, view-original link).
  • src/services/i18n.js — notif.mentioned (nl/en/de).
  • test/inbound-mentions.test.js — detection guards + notification surfacing.
  • CHANGELOG(.nl/.de).md — "A mention is now a notification" under Added.

Closes prutfolio-src-xgg.

Co-Authored-By: Claude <noreply@…>

  • Property mode set to 100644
File size: 2.4 KB
Line 
1// Inbound mentions — a remote note whose Mention tag targets one of our actors becomes a
2// notification, even when it isn't a reply to our content. localMentionSlugs is the detection
3// core: only OUR base counts, the slug must exist, deduped. In-memory SQLite. Run: npm test
4import { test } from 'node:test';
5import assert from 'node:assert/strict';
6
7process.env.DATABASE_PATH = ':memory:';
8process.env.PUBLIC_BASE_URL = 'https://test.example';
9
10const dbMod = await import('../src/config/database.js');
11const db = dbMod.default;
12dbMod.initializeDatabase();
13const AP = (await import('../src/services/ActivityPubService.js')).default;
14
15const BASE = 'https://test.example';
16db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)').run('u1', 'u1', 'u1@test', 'x', 'god');
17db.prepare('INSERT INTO sites (id, slug, title, owner_id, is_primary) VALUES (?,?,?,?,?)').run('s1', 'demo', 'Demo', 'u1', 1);
18
19test('a Mention tag with our actor href resolves to the site slug', () => {
20 const tags = [
21 { type: 'Mention', href: `${BASE}/ap/users/demo`, name: '@demo@test.example' },
22 { type: 'Hashtag', href: `${BASE}/tag/x`, name: '#x' },
23 ];
24 assert.deepEqual(AP.localMentionSlugs(tags, BASE), ['demo']);
25});
26
27test('a remote host with the same /ap/users path is NOT ours', () => {
28 const tags = [{ type: 'Mention', href: 'https://other.example/ap/users/demo', name: '@demo@other.example' }];
29 assert.deepEqual(AP.localMentionSlugs(tags, BASE), []);
30});
31
32test('a mention of a non-existent slug is ignored; duplicates dedupe', () => {
33 const tags = [
34 { type: 'Mention', href: `${BASE}/ap/users/ghost`, name: '@ghost' },
35 { type: 'Mention', href: `${BASE}/ap/users/demo`, name: '@demo' },
36 { type: 'Mention', href: `${BASE}/ap/users/demo`, name: '@demo' },
37 ];
38 assert.deepEqual(AP.localMentionSlugs(tags, BASE), ['demo']);
39});
40
41test('a stored mention shows up in the notifications', () => {
42 db.prepare('INSERT OR IGNORE INTO ap_mentions (slug, object_uri, note_url, actor_uri, actor_name, actor_handle, content) VALUES (?,?,?,?,?,?,?)')
43 .run('demo', 'https://m.example/notes/1', 'https://m.example/@a/1', 'https://m.example/users/a', 'Anna', '@a@m.example', '<p>hi @demo</p>');
44 const items = AP.getNotifications('demo', 20);
45 const m = items.find((n) => n.type === 'mention');
46 assert.ok(m, 'mention notification present');
47 assert.equal(m.handle, '@a@m.example');
48 assert.equal(m.note_url, 'https://m.example/@a/1');
49});
Note: See TracBrowser for help on using the repository browser.