From 4052eebc9db5e35aeb16024c680dbbcb23a4ea59 Mon Sep 17 00:00:00 2001 From: Ricardo Date: Tue, 24 Feb 2026 10:58:13 +0100 Subject: [PATCH] fix: re-fetch media from Fedify when stored timeline item has none Post-detail view now re-fetches from the origin server when the stored timeline item has empty photo/video/audio arrays (from before the async iteration fix). On successful extraction, updates MongoDB so future views don't need to re-fetch. --- lib/controllers/post-detail.js | 82 +++++++++++++++++++++++----------- package.json | 2 +- 2 files changed, 58 insertions(+), 26 deletions(-) diff --git a/lib/controllers/post-detail.js b/lib/controllers/post-detail.js index 04ddece..68b7481 100644 --- a/lib/controllers/post-detail.js +++ b/lib/controllers/post-detail.js @@ -153,7 +153,15 @@ export function postDetailController(mountPath, plugin) { let object = null; - if (!timelineItem) { + // If stored item has no media, re-fetch from Fedify to pick up + // attachments that were missed before the async iteration fix. + const storedHasNoMedia = + timelineItem && + (!timelineItem.photo || timelineItem.photo.length === 0) && + (!timelineItem.video || timelineItem.video.length === 0) && + (!timelineItem.audio || timelineItem.audio.length === 0); + + if (!timelineItem || storedHasNoMedia) { // Not in local timeline — fetch via lookupObject const handle = plugin.options.actor.handle; const ctx = plugin._federation.createContext( @@ -185,7 +193,8 @@ export function postDetailController(mountPath, plugin) { } } - if (!object) { + if (!object && !storedHasNoMedia) { + // Truly not found (no local item either) return response.status(404).render("activitypub-post-detail", { title: response.locals.__("activitypub.reader.post.title"), notFound: true, objectUrl, mountPath, @@ -194,34 +203,57 @@ export function postDetailController(mountPath, plugin) { }); } - // If it's an actor (Person, Service, Application), redirect to profile - if ( - object instanceof Person || - object instanceof Service || - object instanceof Application - ) { - return response.redirect( - `${mountPath}/admin/reader/profile?url=${encodeURIComponent(objectUrl)}`, - ); - } + if (object) { + // If it's an actor (Person, Service, Application), redirect to profile + if ( + object instanceof Person || + object instanceof Service || + object instanceof Application + ) { + return response.redirect( + `${mountPath}/admin/reader/profile?url=${encodeURIComponent(objectUrl)}`, + ); + } - // Extract timeline item data from the Fedify object - if (object instanceof Note || object instanceof Article) { - try { - timelineItem = await extractObjectData(object); - } catch (error) { - console.error(`[post-detail] extractObjectData failed for ${objectUrl}:`, error.message); - return response.status(500).render("error", { + // Extract timeline item data from the Fedify object + if (object instanceof Note || object instanceof Article) { + try { + const freshItem = await extractObjectData(object); + + // If re-fetch found media that the stored item was missing, update MongoDB + if (storedHasNoMedia && timelineCol) { + const hasMedia = + (freshItem.photo && freshItem.photo.length > 0) || + (freshItem.video && freshItem.video.length > 0) || + (freshItem.audio && freshItem.audio.length > 0); + if (hasMedia) { + await timelineCol.updateOne( + { $or: [{ uid: objectUrl }, { url: objectUrl }] }, + { $set: { photo: freshItem.photo, video: freshItem.video, audio: freshItem.audio } }, + ).catch(() => {}); + } + } + + timelineItem = freshItem; + } catch (error) { + // If re-extraction fails but we have a stored item, use it + if (!storedHasNoMedia) { + console.error(`[post-detail] extractObjectData failed for ${objectUrl}:`, error.message); + return response.status(500).render("error", { + title: "Error", + content: "Failed to extract post data", + }); + } + // storedHasNoMedia=true means timelineItem still has the stored data + } + } else if (!storedHasNoMedia) { + return response.status(400).render("error", { title: "Error", - content: "Failed to extract post data", + content: "Object is not a viewable post (must be Note or Article)", }); } - } else { - return response.status(400).render("error", { - title: "Error", - content: "Object is not a viewable post (must be Note or Article)", - }); } + // If object is null and storedHasNoMedia, we fall through with the stored timelineItem } // Build interaction state for this post diff --git a/package.json b/package.json index c648809..71f52df 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@rmdes/indiekit-endpoint-activitypub", - "version": "2.0.18", + "version": "2.0.19", "description": "ActivityPub federation endpoint for Indiekit via Fedify. Adds full fediverse support: actor, inbox, outbox, followers, following, syndication, and Mastodon migration.", "keywords": [ "indiekit",