source: Klonkt/src/views/partials/fedi-node.ejs@ 74c5abc

main
Last change on this file since 74c5abc was 74c5abc, checked in by Robin Genis <roboburr@…>, 2 months ago

feat(images): downscale remote (fediverse) avatars via a signed proxy

Remote avatars live on other servers, so the browser shrank full-res line-art to ~44px
(jagged). Fetch them once (SSRF-safe via safeFetch), downscale identically (lanczos -> WebP),
cache, serve. HMAC-signed proxy URLs → not an open resizer.

  • services/ActivityPubService.js — export safeFetch
  • services/ThumbnailService.js — getRemoteThumbnail + signed imgProxyUrl/verifyImg; +128px size
  • server.js — GET /img/a/:w signed proxy route
  • middleware/render.js — avatar(url,w) helper (local thumb / remote proxy)
  • views: news.ejs, fedi-node.ejs, following.ejs avatars -> avatar()
  • Property mode set to 100644
File size: 4.8 KB
Line 
1<%# Renders one fediverse thread node (n). Expects: n, t, canManageSite, _base, siteAvatar, formatDateTime, postSlug %>
2<div class="comment-avatar">
3 <% if (n.actor_icon) { %><img src="<%= avatar(n.actor_icon, 128) %>" alt="" loading="lazy">
4 <% } else { %><span><%= (n.actor_name || '?').charAt(0).toUpperCase() %></span><% } %>
5</div>
6<div class="comment-body">
7 <div class="comment-meta">
8 <% if (n.actor_url) { %><a class="comment-author" href="<%= n.actor_url %>" rel="nofollow noopener" target="_blank"><%= n.actor_name %></a>
9 <% } else { %><span class="comment-author"><%= n.actor_name %></span><% } %>
10 <% if (n.actor_handle) { %><span class="fedi-handle"><%= n.actor_handle %></span><% } %>
11 <% if (n.created_at) { %><span class="comment-time"><%= formatDateTime(n.created_at) %></span><% } %>
12 </div>
13 <div class="comment-content"><%- n.content %></div>
14 <div class="comment-actions">
15 <% if (n.mine && typeof canManageSite !== 'undefined' && canManageSite && n.outboxId) { %>
16 <form method="post" action="<%= _base %>/fediverse/<%= n.outboxId %>/delete" data-confirm="<%= t('fedi.delete_confirm') %>">
17 <button type="submit" class="comment-delete-btn"><%= t('comments.delete') %></button>
18 </form>
19 <% } else if (!n.mine && typeof canManageSite !== 'undefined' && canManageSite && n.id && typeof postSlug !== 'undefined' && postSlug) { %>
20 <%# Owner: like / boost / reply directly AS the site — no "your server" detour (it's your own site). %>
21 <form method="post" action="<%= _base %>/posts/<%= postSlug %>/fedi-react" class="fedi-owner-react">
22 <input type="hidden" name="interaction_id" value="<%= n.id %>">
23 <input type="hidden" name="kind" value="like">
24 <button type="submit" class="fedi-cact fedi-cact-like<%= n.acted_like ? ' is-on' : '' %>" title="<%= n.acted_like ? t('fedi.unlike_short') : t('fedi.like_short') %>" aria-label="<%= n.acted_like ? t('fedi.unlike_short') : t('fedi.like_short') %>"><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>
25 </form>
26 <form method="post" action="<%= _base %>/posts/<%= postSlug %>/fedi-react" class="fedi-owner-react">
27 <input type="hidden" name="interaction_id" value="<%= n.id %>">
28 <input type="hidden" name="kind" value="boost">
29 <button type="submit" class="fedi-cact fedi-cact-boost<%= n.acted_boost ? ' is-on' : '' %>" title="<%= n.acted_boost ? t('tl.unboost') : t('fedi.boost_short') %>" aria-label="<%= n.acted_boost ? t('tl.unboost') : t('fedi.boost_short') %>"><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>
30 </form>
31 <details class="fedi-owner-reply">
32 <summary class="fedi-cact fedi-cact-reply" title="<%= t('fedi.remote_reply_short') %>" aria-label="<%= t('fedi.remote_reply_short') %>"><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></summary>
33 <form method="post" action="<%= _base %>/posts/<%= postSlug %>/fedi-reply" class="comment-reply-form">
34 <input type="hidden" name="interaction_id" value="<%= n.id %>">
35 <textarea name="text" rows="2" required placeholder="<%= t('fedi.reply_ph') %>"></textarea>
36 <div class="comment-reply-form-actions">
37 <button type="submit" class="btn"><%= t('fedi.send') %></button>
38 </div>
39 </form>
40 </details>
41 <% } else if (!n.mine && n.noteId) { %>
42 <button type="button" class="comment-reply-btn fedi-remote-reply-btn" data-fedi-uri="<%= n.noteId %>" data-fedi-ph="<%= t('fedi.remote_ph') %>">
43 <svg viewBox="0 0 24 24" width="13" height="13" 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>
44 <%= t('fedi.remote_reply_short') %>
45 </button>
46 <% } %>
47 </div>
48 <% if (n.children && n.children.length) { %>
49 <ol class="comment-replies">
50 <% n.children.forEach(function(c){ %>
51 <li class="comment comment-reply"><%- include('../partials/fedi-node', { n: c, t: t, canManageSite: (typeof canManageSite !== 'undefined' ? canManageSite : false), _base: _base, siteAvatar: (typeof siteAvatar !== 'undefined' ? siteAvatar : null), formatDateTime: formatDateTime, postSlug: (typeof postSlug !== 'undefined' ? postSlug : null) }) %></li>
52 <% }); %>
53 </ol>
54 <% } %>
55</div>
Note: See TracBrowser for help on using the repository browser.