Changeset 3d7312a in Klonkt


Ignore:
Timestamp:
06/24/2026 12:44:21 PM (3 months ago)
Author:
Robin Genis <roboburr@…>
Branches:
main
Children:
594cc50
Parents:
82079ad
Message:

feat(activitypub): remote interaction — reply to any fediverse post from your own server

Standard 'reply from your own server' flow: every post page has a 'Reply via the
fediverse' button that bounces the visitor to their home server's
/authorize_interaction?uri=. Klonkt implements that endpoint: the site owner
composes a reply that's resolved (resolveRemoteNote) + federated back to the
remote post as the site actor. Works Klonkt<->Klonkt and with Mastodon.

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

Location:
src
Files:
1 added
6 edited

Legend:

Unmodified
Added
Removed
  • src/assets/css/style.css

    r82079ad r3d7312a  
    44174417.post-fediverse .fedi-reply-toggle::-webkit-details-marker { display: none; }
    44184418.post-fediverse details[open] .fedi-reply-toggle { margin-bottom: .5rem; }
     4419
     4420/* Reply-via-fediverse CTA on post pages */
     4421.post-fediverse-cta { margin: 1.75rem 0; }
     4422.fedi-remote-reply-btn { display: inline-flex; align-items: center; gap: .4rem; }
  • src/routes/posts.js

    r82079ad r3d7312a  
    8686  'tag', 'type', 'user', 'users', 'artiesten', 'leden', 'favorieten', 'feed.xml', 'atom.xml', 'sitemap.xml',
    8787  'manifest.webmanifest', 'sw.js', 'favicon.ico', 'favicon.svg', 'assets',
     88  'authorize_interaction',
    8889]);
    8990
     
    509510  return { newerPost, olderPost };
    510511}
     512
     513// ==================== REMOTE INTERACTION (reply to a fediverse post as your site) ====================
     514// Standard fediverse "reply from your own server" landing endpoint. A post page
     515// elsewhere bounces the visitor here with ?uri=<remote post>; the site owner
     516// composes a reply that federates back to that post.
     517router.get('/authorize_interaction', requireSiteManager, async (req, res) => {
     518  const site = res.locals.site;
     519  const uri = (req.query.uri || '').toString();
     520  let target = null;
     521  try { target = await ActivityPubService.resolveRemoteNote(uri); } catch { /* ignore */ }
     522  renderPage(req, res, 'pages/authorize-interaction', {
     523    pageTitle: 'Reageer via de fediverse',
     524    bodyClass: 'on-special',
     525    uri,
     526    target,
     527    siteTitle: site ? site.title : '',
     528  });
     529});
     530
     531router.post('/authorize_interaction', requireSiteManager, async (req, res) => {
     532  const site = res.locals.site;
     533  const uri = (req.body.uri || '').toString();
     534  const text = (req.body.text || '').toString();
     535  if (site && uri && text.trim()) {
     536    try {
     537      const parent = await ActivityPubService.resolveRemoteNote(uri);
     538      if (parent) await ActivityPubService.deliverReply(site, { postId: '', postSlug: null, parent, text });
     539    } catch (e) { console.warn('[AP] remote reply failed:', e.message); }
     540  }
     541  // Send them to the post they replied to so they can see the thread.
     542  res.redirect(uri && /^https?:\/\//i.test(uri) ? uri : (res.locals.siteUrlBase || '/'));
     543});
    511544
    512545// ==================== VIEW POST (last route — catches /:slug) ====================
  • src/services/ActivityPubService.js

    r82079ad r3d7312a  
    477477}
    478478
     479// Resolve a remote post URL (any fediverse/Klonkt post) into a reply target.
     480// Returns a parent-shaped object usable by deliverReply(), or null.
     481export async function resolveRemoteNote(url) {
     482  if (!/^https?:\/\//i.test(String(url || ''))) return null;
     483  const note = await fetchActor(url).catch(() => null); // AP GET (content-negotiates)
     484  if (!note || !note.id) return null;
     485  const att = note.attributedTo;
     486  const actorUri = typeof att === 'string' ? att : (att && att.id);
     487  if (!actorUri) return null;
     488  const actor = await fetchActor(actorUri).catch(() => null);
     489  const ai = actorInfo(actor, actorUri);
     490  return {
     491    object_uri: note.id,
     492    actor_uri: actorUri,
     493    actor_url: ai.url,
     494    actor_handle: ai.handle,
     495    actor_name: ai.name,
     496    actor_icon: ai.icon,
     497    preview: HtmlSanitizerService.toPlainText(note.content || '').slice(0, 240),
     498  };
     499}
     500
    479501export default {
    480502  getOrCreateKeys, apWants, sendAP, actorId, noteId,
    481503  buildActor, buildNote, buildCreate, buildOutbox, buildFollowers,
    482504  followerCount, deliver, fetchActor, verifyRequest, handleInbox, deliverCreate, deliverDelete,
    483   getInteractions, getInteractionById, buildReplyNote, getOutboxNote, deliverReply,
     505  getInteractions, getInteractionById, buildReplyNote, getOutboxNote, deliverReply, resolveRemoteNote,
    484506};
  • src/services/i18n.js

    r82079ad r3d7312a  
    109109    'fedi.heading': 'Vanuit de fediverse', 'fedi.likes': 'sterren', 'fedi.boosts': 'boosts', 'fedi.replies': 'Reacties uit de fediverse',
    110110    'fedi.reply': 'Reageer', 'fedi.reply_ph': 'Je antwoord aan de fediverse…', 'fedi.send': 'Versturen', 'fedi.you': 'Jij',
     111    'fedi.remote_title': 'Reageer via de fediverse', 'fedi.remote_reply': 'Reageer via de fediverse', 'fedi.remote_prompt': 'Je fediverse-adres (bv. @jij@mastodon.social):', 'fedi.remote_notfound': 'Kon die post niet ophalen. Plak de volledige post-URL:', 'fedi.remote_load': 'Ophalen', 'fedi.remote_replying_to': 'Je reageert op', 'fedi.remote_as': 'Wordt verzonden als {site}.',
    111112    'comments.to_start': 'om de conversatie te starten.',
    112113    'comments.reply': 'Reageer', 'comments.delete': 'Verwijder', 'comments.cancel': 'Annuleren',
     
    11031104    'fedi.heading': 'From the fediverse', 'fedi.likes': 'favourites', 'fedi.boosts': 'boosts', 'fedi.replies': 'Replies from the fediverse',
    11041105    'fedi.reply': 'Reply', 'fedi.reply_ph': 'Your reply to the fediverse…', 'fedi.send': 'Send', 'fedi.you': 'You',
     1106    'fedi.remote_title': 'Reply via the fediverse', 'fedi.remote_reply': 'Reply via the fediverse', 'fedi.remote_prompt': 'Your fediverse address (e.g. @you@mastodon.social):', 'fedi.remote_notfound': 'Could not fetch that post. Paste the full post URL:', 'fedi.remote_load': 'Fetch', 'fedi.remote_replying_to': 'Replying to', 'fedi.remote_as': 'Sent as {site}.',
    11051107    'comments.to_start': 'to start the conversation.',
    11061108    'comments.reply': 'Reply', 'comments.delete': 'Delete', 'comments.cancel': 'Cancel',
     
    20952097    'fedi.heading': 'Aus dem Fediverse', 'fedi.likes': 'Favoriten', 'fedi.boosts': 'Boosts', 'fedi.replies': 'Antworten aus dem Fediverse',
    20962098    'fedi.reply': 'Antworten', 'fedi.reply_ph': 'Deine Antwort an das Fediverse…', 'fedi.send': 'Senden', 'fedi.you': 'Du',
     2099    'fedi.remote_title': 'Über das Fediverse antworten', 'fedi.remote_reply': 'Über das Fediverse antworten', 'fedi.remote_prompt': 'Deine Fediverse-Adresse (z.B. @du@mastodon.social):', 'fedi.remote_notfound': 'Beitrag konnte nicht geladen werden. Füge die vollständige Beitrags-URL ein:', 'fedi.remote_load': 'Laden', 'fedi.remote_replying_to': 'Antwort an', 'fedi.remote_as': 'Wird als {site} gesendet.',
    20972100    'comments.to_start': 'um das Gespräch zu starten.',
    20982101    'comments.reply': 'Antworten', 'comments.delete': 'Löschen', 'comments.cancel': 'Abbrechen',
  • src/views/pages/post.ejs

    r82079ad r3d7312a  
    135135    </section>
    136136  <% } %>
     137
     138  <!-- Reply via the fediverse (remote interaction: bounces to the visitor's own server) -->
     139  <% var _postAbsUrl = (typeof ogOrigin !== 'undefined' && ogOrigin ? ogOrigin : '') + (typeof _base !== 'undefined' && _base ? _base : '') + '/' + post.slug; %>
     140  <div class="post-fediverse-cta">
     141    <button type="button" class="btn fedi-remote-reply-btn"
     142      onclick="(function(){var d=prompt(<%- JSON.stringify(t('fedi.remote_prompt')) %>);if(!d)return;d=d.trim().replace(/^@?[^@\s]*@/,'').replace(/^https?:\/\//i,'').replace(/\/.*$/,'').trim();if(d)location.href='https://'+d+'/authorize_interaction?uri='+encodeURIComponent(<%- JSON.stringify(_postAbsUrl) %>);})()">
     143      🐘 <%= t('fedi.remote_reply') %>
     144    </button>
     145  </div>
    137146
    138147  <!-- Comments -->
  • src/views/shell.ejs

    r82079ad r3d7312a  
    175175
    176176<!-- v9 stylesheet (full palette system) -->
    177 <link rel="stylesheet" href="/assets/css/style.css?v=38">
     177<link rel="stylesheet" href="/assets/css/style.css?v=39">
    178178
    179179<!-- Audio player styles: loaded on every page so the mini-player works
Note: See TracChangeset for help on using the changeset viewer.