Ignore:
Timestamp:
06/27/2026 12:04:36 PM (2 months ago)
Author:
Robin Genis <roboburr@…>
Branches:
main
Children:
dd7daef
Parents:
7cea873
Message:

feat(fedi): edit your own outbound replies (Update(Note))

  • services/ActivityPubService.js — deliverOutboxUpdate rewrites the stored reply (mention re-added + #tags re-linked) and federates an Update(Note); listOutbox exposes the plain editable text (outboxEditableText) to prefill the box; added to the default export.
  • routes/posts.js — POST /fediverse/:id/edit (owner only).
  • views/pages/authorize-interaction.ejs — an Edit details/textarea per reply in the manage list; + CSS.
  • services/i18n.js — fedi.edit / fedi.save_edit (NL/EN/DE).
File:
1 edited

Legend:

Unmodified
Added
Removed
  • src/services/ActivityPubService.js

    r7cea873 rbddbfe0  
    10761076
    10771077// List a site's own outbound fediverse replies (for the manage/delete view).
     1078// The plain editable text of a stored reply (unwrap links → their text, <br> → newline)
     1079// so the manage view can prefill an edit box; the mention is re-added on save.
     1080function outboxEditableText(content) {
     1081  return String(content || '')
     1082    .replace(/<br\s*\/?>/gi, '\n')
     1083    .replace(/<a\b[^>]*>([\s\S]*?)<\/a>/gi, '$1')
     1084    .replace(/<[^>]+>/g, '')
     1085    .replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&amp;/g, '&')
     1086    .trim();
     1087}
    10781088export function listOutbox(siteSlug) {
    10791089  return db.prepare('SELECT id, content, to_handle, in_reply_to, created_at FROM ap_outbox WHERE site_slug = ? ORDER BY created_at DESC')
    1080     .all(siteSlug).map((r) => ({ ...r, content: stripLeadingMentions(r.content) }));
     1090    .all(siteSlug).map((r) => { const c = stripLeadingMentions(r.content); return { ...r, content: c, editable: outboxEditableText(c) }; });
    10811091}
    10821092
     
    10981108  db.prepare('DELETE FROM ap_outbox WHERE id = ?').run(outboxId);
    10991109  return true;
     1110}
     1111
     1112// Edit one of our outbound replies: rewrite the stored content (mention re-added + #tags
     1113// re-linked) and send an Update(Note) so recipients refresh their cached copy.
     1114export async function deliverOutboxUpdate(site, outboxId, newText) {
     1115  const row = iStmts().getO.get(outboxId);
     1116  if (!row || row.site_slug !== site.slug) return false;
     1117  const text = String(newText || '').trim();
     1118  if (!text) return false;
     1119  const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, '');
     1120  if (!base) return false;
     1121  const me = actorId(base, site.slug);
     1122  const mention = row.to_actor
     1123    ? `<a href="${escHtml(row.to_actor)}" class="u-url mention">${escHtml(row.to_handle || deriveHandle(row.to_actor))}</a> ` : '';
     1124  const body = escHtml(text).replace(/\r?\n/g, '<br>');
     1125  const content = `<p>${mention}${linkHashtags(base, body)}</p>`;
     1126  db.prepare('UPDATE ap_outbox SET content = ? WHERE id = ?').run(content, outboxId);
     1127  const note = buildReplyNote(base, site, iStmts().getO.get(outboxId));
     1128  note.updated = new Date().toISOString();
     1129  const update = {
     1130    '@context': 'https://www.w3.org/ns/activitystreams',
     1131    id: `${note.id}#update-${Date.now()}-${rid()}`, type: 'Update', actor: me,
     1132    published: note.published, updated: note.updated, to: note.to, cc: note.cc, object: note,
     1133  };
     1134  const keys = getOrCreateKeys(site.slug);
     1135  const inboxes = new Set();
     1136  if (row.to_actor) { const a = await fetchActor(row.to_actor).catch(() => null); if (a) inboxes.add((a.endpoints && a.endpoints.sharedInbox) || a.inbox); }
     1137  for (const f of fStmts().list.all(site.slug)) inboxes.add(f.shared_inbox || f.inbox);
     1138  inboxes.delete(`${me}/inbox`); inboxes.delete(`${base}/ap/inbox`);
     1139  let delivered = 0;
     1140  for (const inbox of [...inboxes].filter(Boolean)) {
     1141    try { const st = await deliver(inbox, update, `${me}#main-key`, keys.private_pem); if (st >= 200 && st < 300) delivered++; } catch { /* best-effort */ }
     1142  }
     1143  console.log('[AP] outreply edit', site.slug, 'delivered', delivered);
     1144  return { ok: true, content, delivered };
    11001145}
    11011146
     
    14531498  followerCount, deliver, fetchActor, verifyRequest, handleInbox, deliverCreate, deliverDelete, deliverUpdate, deliverActorUpdate, resyncFeaturedPins,
    14541499  getInteractions, getInteractionById, setInteractionBoosted, setInteractionLiked, setMyReaction, getMyReactions, buildReplyNote, getOutboxNote, deliverReply, resolveRemoteNote,
    1455   listOutbox, deliverOutboxDelete,
     1500  listOutbox, deliverOutboxDelete, deliverOutboxUpdate,
    14561501  webfingerResolve, followActor, resolveRemoteActor, unfollowActor, listFollowing, setAutoBoost, getTimeline, sendInteraction,
    14571502  autoBoostCount, boostedCount, markBoosted, unmarkBoosted, markLiked, unmarkLiked, getTimelineReaction, upsertBoostedNote, getCirkelPosts, getCirkelMembers, selfHealTimeline,
Note: See TracChangeset for help on using the changeset viewer.