source: Klonkt/src/views/partials/tl-item.ejs@ b258a79

main
Last change on this file since b258a79 was b258a79, checked in by Robin Genis <roboburr@…>, 6 weeks ago

Fase 3: quotes federeren volgens de FEP, en de geciteerde auteur hoort het

De spiegel van wat we al aan de ingest-kant deden. Quoot je vanuit Klonkt een
fediverse-object, dan gaat dat nu ook echt de lijn over als quote, in de drie
vormen die het netwerk werkelijk leest: de FEP-044f quote-property, de de-facto
aliassen quoteUrl en _misskey_quote, en een FEP-e232 Link-tag. Alle drie wijzen
naar hetzelfde object, want elke lezer kijkt ergens anders.

En het belangrijkste: de geciteerde auteur komt in cc en zijn inbox in de
bezorglijst. Geciteerd worden zonder het te horen is precies de onbeleefdheid
die deze FEP probeert weg te ontwerpen.

De resolutie gebeurt een keer, bij publiceren (deliverCreate is toch al async),
en wordt opgeslagen op de post. buildNote blijft daardoor synchroon en hoeft
nooit te fetchen, ook niet als de outbox 'm opnieuw opbouwt.

Web-UI: een externe embed gebruikt nu dezelfde .tl-quote-kaart als een quote, dus
de belofte van een representatie klopt nu ook op het web.

Veiligheid: titel, provider en auteursnaam komen van een derde partij en worden
als PLATTE TEKST opgeslagen (tags eruit, lengte begrensd), zodat geen enkele
renderer verderop degene hoeft te zijn die aan escapen denkt. In de web-template
gaat de titel er bovendien nog een keer ge-escaped in, want de quote-kaart
rendert content als HTML (terecht voor AP-content, die we bij binnenkomst
saneren, maar niet voor dit).

Changed files:
src/config/database.js

  • posts.quote_uri en posts.quote_actor

src/services/ActivityPubService.js

  • applyQuoteProps: de drie quote-vormen + de auteur in cc
  • buildNote past ze toe; resolveOwnQuote resolvet een keer bij publiceren
  • deliverCreate bewaart het resultaat en bezorgt bij de geciteerde auteur
  • externe embed slaat titel/provider/auteur als platte tekst op

src/views/partials/tl-item.ejs

  • embed valt terug op dezelfde quote-kaart, met ge-escapete titel

test/external-embeds.test.js

  • 4 tests erbij: de drie vormen, de auteur in cc zonder de followers te verliezen, junk-invoer verandert niets, en er wordt nooit markup opgeslagen

remarks: 209 tests groen.

-robo
Co-Authored-By: Claude Opus 4.8 <noreply@…>

  • Property mode set to 100644
File size: 8.3 KB
RevLine 
[7b04d3b]1<li class="tl-item">
[3597763]2 <% if (p.reblog_name) { %><div class="tl-boost-by"><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="17 1 21 5 17 9"/><path d="M3 11V9a4 4 0 0 1 4-4h14"/><polyline points="7 23 3 19 7 15"/><path d="M21 13v2a4 4 0 0 1-4 4H3"/></svg> <strong><%- emojiName(p.reblog_name, p.reblog_emoji_json) %></strong> <%= t('tl.boosted') %></div><% } %>
[7b04d3b]3 <div class="tl-head">
4 <span class="tl-avatar"><% if (p.author_icon) { %><img src="<%= avatar(p.author_icon, 96) %>" alt="" loading="lazy"><% } else { %><%= (p.author_name || '?').charAt(0).toUpperCase() %><% } %></span>
5 <span class="tl-id">
[3597763]6 <a class="tl-author" href="<%= p.author_url || p.author_uri %>" target="_blank" rel="nofollow noopener"><%- emojiName(p.author_name, p.author_emoji_json) %></a>
[7b04d3b]7 <span class="tl-handle"><%= p.author_handle %></span>
8 </span>
9 <% if (p.published || p.created_at) { %><time class="tl-time"><%= formatDateTime(p.published || p.created_at) %></time><% } %>
10 </div>
11
12 <%
13 var media = []; try { media = JSON.parse(p.media_json || '[]'); } catch (e) { media = []; }
14 var imgs = media.filter(function(m){ return m && m.url && (!m.type || /^image\//.test(m.type)); });
15 var vids = media.filter(function(m){ return m && m.url && m.type && /^video\//.test(m.type); });
16 var auds = media.filter(function(m){ return m && m.url && m.type && /^audio\//.test(m.type); });
17 var _nsfwVisual = !!p.nsfw && (imgs.length || vids.length || auds.length);
18 var _nsfwText = !!p.nsfw && !(imgs.length || vids.length || auds.length);
19 %>
20 <% if (_nsfwText) { %>
[3597763]21 <div class="tl-content nsfw-media"><span class="nsfw-veil"><%- include('../partials/nsfw-veil', { cw: p.cw }) %></span><%- emojiHtml(p.content, p.emoji_json) %></div>
[7b04d3b]22 <% } else { %>
[3597763]23 <div class="tl-content"><%- emojiHtml(p.content, p.emoji_json) %></div>
[7b04d3b]24 <% } %>
[b258a79]25 <%
26 // One card for both: a fediverse quote and an external embed look
27 // identical; only where they came from differs. An embed carries a
28 // title and a thumbnail, never an iframe.
29 var _quote = noteQuote(p.quote_json);
30 if (!_quote) {
31 var _emb = noteQuote(p.embed_json);
32 // The title comes from a third-party oEmbed provider, so it is
33 // escaped here: the quote card renders `content` as HTML (fine for
34 // AP content, which we sanitise on the way in, but not for this).
35 if (_emb) _quote = { url: _emb.url, author: _emb.author || (_emb.provider ? { name: _emb.provider } : null),
36 content: _emb.title ? ('<p>' + emojiName(_emb.title, null) + '</p>') : '', media: _emb.media || [] };
37 }
38 %>
39 <% if (_quote) { %><%- include('../partials/quote-card', { q: _quote }) %><% } %>
[7b04d3b]40 <% if (_nsfwVisual) { %><div class="nsfw-media"><% } %>
41 <% if (imgs.length) { %>
42 <div class="tl-media">
43 <% imgs.forEach(function(m){ %>
44 <a class="tl-media-img" href="<%= m.url %>" target="_blank" rel="noopener"><img src="<%= thumb(m.url, 1280) %>" alt="" loading="lazy" decoding="async"></a>
45 <% }); %>
46 </div>
47 <% } %>
48 <% vids.forEach(function(m){ %><video class="tl-media-video" src="<%= m.url %>" poster="<%= thumb(m.url, 1280) %>" controls preload="metadata" playsinline></video><% }); %>
49 <% /* A Klonkt audio post carries an embed player (embedHtml/embedUrl) that already
50 covers these tracks, so don't ALSO render the raw Audio attachments as bare
51 players here. Posts without a Klonkt embed (e.g. a plain remote audio) keep them. */ %>
52 <% if (!p.embedHtml && !p.embedUrl) { auds.forEach(function(m){ %><audio class="tl-media-audio" src="<%= m.url %>" controls preload="none"></audio><% }); } %>
53 <% if (_nsfwVisual) { %><div class="nsfw-veil"><%- include('../partials/nsfw-veil', { cw: p.cw }) %></div></div><% } %>
54
55 <% if (p.embedHtml) { %><div class="tl-embed"><%- p.embedHtml %></div><% } %>
56 <% if (p.embedUrl) { %><a class="tl-embed-open" href="<%= p.embedUrl %>" target="_blank" rel="noopener">▶ <%= t('tl.open_player') %></a><% } %>
57
58 <% if (p.poll) { var _pl=p.poll; var _tot=_pl.options.reduce(function(s,o){return s+(o.count||0);},0); var _voteable=!_pl.closed&&!_pl.voted; %>
59 <div class="tl-poll">
60 <% if (_voteable) { %>
61 <form method="post" action="/news/vote" class="tl-poll-form">
62 <input type="hidden" name="note" value="<%= p.id %>">
63 <% _pl.options.forEach(function(o){ %><label class="tl-poll-choice"><input type="<%= _pl.multiple?'checkbox':'radio' %>" name="choice" value="<%= o.name %>"><span><%= o.name %></span></label><% }); %>
64 <button type="submit" class="btn btn-primary tl-poll-btn"><%= t('poll.vote') %></button>
65 </form>
66 <% } else { _pl.options.forEach(function(o){ var _pct=_tot?Math.round((o.count||0)*100/_tot):0; var _mine=_pl.voted&&(Array.isArray(_pl.voted)?_pl.voted.indexOf(o.name)>=0:_pl.voted===o.name); %><div class="tl-poll-res<%= _mine?' is-mine':'' %>"><span class="tl-poll-fill" style="width:<%= _pct %>%"></span><span class="tl-poll-name"><%= _mine?'✓ ':'' %><%= o.name %></span><span class="tl-poll-pct"><%= _pct %>%</span></div><% }); } %>
67 <div class="tl-poll-foot"><%= (_pl.voters!=null?_pl.voters:_tot) %> <%= t('poll.votes') %><% if(_pl.closed){ %> · <%= t('poll.closed') %><% } %></div>
68 </div>
69 <% } %>
70
71 <% if (p.url) { %><a class="tl-orig" href="<%= p.url %>" target="_blank" rel="nofollow noopener"><%= t('tl.view_original') %></a><% } %>
72
73 <div class="tl-actions">
74 <form method="post" action="/news/like" class="tl-act-form tl-react-form"><input type="hidden" name="note" value="<%= p.id %>"><input type="hidden" name="author" value="<%= p.author_uri %>"><button type="submit" class="tl-act tl-act-like<%= p.liked ? ' is-on' : '' %>" title="<%= p.liked ? t('fedi.unlike_short') : t('fedi.likes') %>" aria-label="<%= p.liked ? t('fedi.unlike_short') : t('fedi.likes') %>"><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2.6l2.9 5.88 6.49.95-4.7 4.58 1.11 6.46L12 17.96l-5.8 3.06 1.1-6.46-4.69-4.58 6.49-.95z"/></svg></button></form>
75 <form method="post" action="/news/boost" class="tl-act-form tl-react-form"><input type="hidden" name="note" value="<%= p.id %>"><input type="hidden" name="author" value="<%= p.author_uri %>"><button type="submit" class="tl-act tl-act-boost<%= p.boosted ? ' is-on' : '' %>" title="<%= p.boosted ? t('tl.unboost') : t('fedi.boosts') %>" aria-label="<%= p.boosted ? t('tl.unboost') : t('fedi.boosts') %>"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="17 1 21 5 17 9"/><path d="M3 11V9a4 4 0 0 1 4-4h14"/><polyline points="7 23 3 19 7 15"/><path d="M21 13v2a4 4 0 0 1-4 4H3"/></svg></button></form>
76 <button type="button" class="tl-act tl-act-reply fedi-remote-reply-btn" data-fedi-uri="<%= p.id %>" data-fedi-ph="<%= t('fedi.remote_ph') %>" title="<%= t('fedi.remote_reply') %>" aria-label="<%= t('fedi.remote_reply') %>"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="9 17 4 12 9 7"/><path d="M20 18v-2a4 4 0 0 0-4-4H4"/></svg></button>
77 <form method="post" action="/blocking/add" class="tl-act-form" data-confirm="<%= t('tl.block') %>?"><input type="hidden" name="target" value="<%= p.author_uri %>"><button type="submit" class="tl-act tl-act-block" title="<%= t('tl.block') %>" aria-label="<%= t('tl.block') %>"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><line x1="4.9" y1="4.9" x2="19.1" y2="19.1"/></svg></button></form>
78 </div>
79 </li>
Note: See TracBrowser for help on using the repository browser.