| 1 | // FEP-e232: Klonkt keeps inbound object-link (quote/ref) tags and serves them on the C2S inbox read.
|
|---|
| 2 | import { test } from 'node:test';
|
|---|
| 3 | import assert from 'node:assert/strict';
|
|---|
| 4 | process.env.DATABASE_PATH = ':memory:';
|
|---|
| 5 | process.env.PUBLIC_BASE_URL = 'https://test.example';
|
|---|
| 6 | const dbMod = await import('../src/config/database.js');
|
|---|
| 7 | dbMod.initializeDatabase();
|
|---|
| 8 | const { extractObjectLinkTags, timelineObjectLinks, extractQuoteUrl, extractLinkJson, quoteHrefOf, timelineQuote } = await import('../src/services/ActivityPubService.js');
|
|---|
| 9 | const AP = { extractObjectLinkTags, timelineObjectLinks, extractQuoteUrl, extractLinkJson, quoteHrefOf, timelineQuote };
|
|---|
| 10 |
|
|---|
| 11 | test('extractObjectLinkTags keeps AS2-profiled ld+json and activity+json Links; drops plain links and mentions', () => {
|
|---|
| 12 | const tag = [
|
|---|
| 13 | { type: 'Link', mediaType: 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', href: 'https://s/objects/1', name: '#1374' },
|
|---|
| 14 | { type: 'Link', mediaType: 'application/activity+json', href: 'https://s/objects/2', rel: ['https://misskey-hub.net/ns#_misskey_quote'] },
|
|---|
| 15 | { type: 'Link', mediaType: 'text/html', href: 'https://plain.example/page' }, // plain hyperlink → dropped
|
|---|
| 16 | { type: 'Mention', href: 'https://s/u/x' },
|
|---|
| 17 | ];
|
|---|
| 18 | const json = AP.extractObjectLinkTags(tag);
|
|---|
| 19 | assert.ok(json);
|
|---|
| 20 | const back = AP.timelineObjectLinks(json);
|
|---|
| 21 | assert.equal(back.length, 2);
|
|---|
| 22 | assert.equal(back[0].href, 'https://s/objects/1');
|
|---|
| 23 | assert.equal(back[0].name, '#1374');
|
|---|
| 24 | assert.equal(back[1].mediaType, 'application/activity+json');
|
|---|
| 25 | });
|
|---|
| 26 |
|
|---|
| 27 | test('no object links → null / undefined (nothing served)', () => {
|
|---|
| 28 | assert.equal(AP.extractObjectLinkTags([{ type: 'Link', mediaType: 'text/html', href: 'https://x' }]), null);
|
|---|
| 29 | assert.equal(AP.extractObjectLinkTags([{ type: 'Hashtag', name: '#hi' }]), null);
|
|---|
| 30 | assert.equal(AP.timelineObjectLinks(null), undefined);
|
|---|
| 31 | assert.equal(AP.timelineObjectLinks('not json'), undefined);
|
|---|
| 32 | });
|
|---|
| 33 |
|
|---|
| 34 | // FEP-044f: object-level quote properties are the common representation.
|
|---|
| 35 | test('extractQuoteUrl reads quote / quoteUrl / quoteUri / _misskey_quote (string or embedded object)', () => {
|
|---|
| 36 | assert.equal(AP.extractQuoteUrl({ quote: 'https://s/objects/9' }), 'https://s/objects/9');
|
|---|
| 37 | assert.equal(AP.extractQuoteUrl({ quoteUrl: 'https://s/q1' }), 'https://s/q1');
|
|---|
| 38 | assert.equal(AP.extractQuoteUrl({ quoteUri: 'https://s/q2' }), 'https://s/q2');
|
|---|
| 39 | assert.equal(AP.extractQuoteUrl({ _misskey_quote: 'https://s/q3' }), 'https://s/q3');
|
|---|
| 40 | assert.equal(AP.extractQuoteUrl({ quote: { type: 'Link', href: 'https://s/q4' } }), 'https://s/q4');
|
|---|
| 41 | assert.equal(AP.extractQuoteUrl({ content: 'no quote' }), null);
|
|---|
| 42 | });
|
|---|
| 43 |
|
|---|
| 44 | test('extractLinkJson normalises an object-level quote into one FEP-e232 Link (rel _misskey_quote)', () => {
|
|---|
| 45 | const json = AP.extractLinkJson({ content: 'nice', quoteUrl: 'https://s/objects/9' });
|
|---|
| 46 | const arr = AP.timelineObjectLinks(json);
|
|---|
| 47 | assert.equal(arr.length, 1);
|
|---|
| 48 | assert.equal(arr[0].href, 'https://s/objects/9');
|
|---|
| 49 | assert.ok(arr[0].rel.some((r) => r.includes('quote')));
|
|---|
| 50 | });
|
|---|
| 51 |
|
|---|
| 52 | test('extractLinkJson merges a real FEP-e232 Link with an object-level quote, deduped by href', () => {
|
|---|
| 53 | const note = {
|
|---|
| 54 | tag: [{ type: 'Link', mediaType: 'application/activity+json', href: 'https://s/objects/9' }],
|
|---|
| 55 | quoteUrl: 'https://s/objects/9', // same target → not duplicated
|
|---|
| 56 | };
|
|---|
| 57 | const arr = AP.timelineObjectLinks(AP.extractLinkJson(note));
|
|---|
| 58 | assert.equal(arr.length, 1);
|
|---|
| 59 | assert.equal(arr[0].href, 'https://s/objects/9');
|
|---|
| 60 | });
|
|---|
| 61 |
|
|---|
| 62 | // FEP-044f embedded quote card: the quoted-post URL feeds the resolver.
|
|---|
| 63 | test('quoteHrefOf reads an object-level quote or a quote-rel FEP-e232 Link', () => {
|
|---|
| 64 | assert.equal(AP.quoteHrefOf({ quoteUrl: 'https://s/q1' }), 'https://s/q1');
|
|---|
| 65 | assert.equal(AP.quoteHrefOf({
|
|---|
| 66 | tag: [{ type: 'Link', href: 'https://s/q2', rel: 'https://misskey-hub.net/ns#_misskey_quote' }],
|
|---|
| 67 | }), 'https://s/q2');
|
|---|
| 68 | assert.equal(AP.quoteHrefOf({ content: 'plain, no quote' }), null);
|
|---|
| 69 | // a plain (non-quote) FEP-e232 reference is not a quote target
|
|---|
| 70 | assert.equal(AP.quoteHrefOf({ tag: [{ type: 'Link', href: 'https://s/ref', rel: 'mention' }] }), null);
|
|---|
| 71 | });
|
|---|
| 72 |
|
|---|
| 73 | test('timelineQuote round-trips the stored snapshot; junk → undefined', () => {
|
|---|
| 74 | const snap = JSON.stringify({ url: 'https://s/q', author: { name: 'A', handle: '@a@s', icon: null }, content: '<p>hi</p>' });
|
|---|
| 75 | const back = AP.timelineQuote(snap);
|
|---|
| 76 | assert.equal(back.url, 'https://s/q');
|
|---|
| 77 | assert.equal(back.author.handle, '@a@s');
|
|---|
| 78 | assert.equal(AP.timelineQuote(null), undefined);
|
|---|
| 79 | assert.equal(AP.timelineQuote('not json'), undefined);
|
|---|
| 80 | });
|
|---|