From 481603391e624958aa37ab5b5582d21675cbd855 Mon Sep 17 00:00:00 2001 From: svemagie <869694+svemagie@users.noreply.github.com> Date: Sun, 22 Mar 2026 20:57:39 +0100 Subject: [PATCH] =?UTF-8?q?fix(mastodon-api):=20DM=20response=20"no=20data?= =?UTF-8?q?"=20=E2=80=94=20return=20full=20serialized=20status?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The minimal bare JSON returned for visibility=direct DMs caused clients (Phanpy, Elk) to show "no data" — they expect a full Mastodon Status entity. Fix: build a proper ap_timeline document, store it with visibility=direct (home/public timelines already exclude direct items), and serialize it via serializeStatus() before returning. Also store the DM in ap_notifications for the thread view as before. Co-Authored-By: Claude Sonnet 4.6 --- lib/mastodon/routes/statuses.js | 59 +++++++++++++++++++++++++-------- 1 file changed, 46 insertions(+), 13 deletions(-) diff --git a/lib/mastodon/routes/statuses.js b/lib/mastodon/routes/statuses.js index 0c4c264..64fcb41 100644 --- a/lib/mastodon/routes/statuses.js +++ b/lib/mastodon/routes/statuses.js @@ -284,12 +284,14 @@ router.post("/api/v1/statuses", async (req, res, next) => { }); console.info(`[Mastodon API] Sent DM to ${recipientActorUrl}`); - // Store in ap_notifications so it appears in the DM thread view + const now = new Date().toISOString(); + const hostname = new URL(publicationUrl).hostname; + const profile = await collections.ap_profile.findOne({}); + + // Store in ap_notifications for the DM thread view try { const ap_notifications = collections.ap_notifications; if (ap_notifications) { - const hostname = new URL(publicationUrl).hostname; - const profile = await collections.ap_profile.findOne({}); await addNotification({ ap_notifications }, { uid: noteId.href, url: noteId.href, @@ -303,23 +305,54 @@ router.post("/api/v1/statuses", async (req, res, next) => { actorHandle: `@${handle}@${hostname}`, inReplyTo: inReplyTo || null, content: { text: (statusText || "").trim(), html: (statusText || "").trim() }, - published: new Date().toISOString(), - createdAt: new Date().toISOString(), + published: now, + createdAt: now, }); } } catch (storeError) { - console.warn("[Mastodon API] Failed to store outbound DM:", storeError.message); + console.warn("[Mastodon API] Failed to store outbound DM in notifications:", storeError.message); } - // Return a minimal status object so the client doesn't error - return res.json({ - id: noteId.href, - created_at: new Date().toISOString(), - content: (statusText || "").trim(), - visibility: "direct", + // Store in ap_timeline with visibility=direct so serializeStatus can + // produce a full Mastodon status object. Home/public timelines already + // exclude direct-visibility items (visibility: { $nin: ["direct"] }). + const timelineItem = { + uid: noteId.href, url: noteId.href, - account: { acct: handle }, + type: "note", + visibility: "direct", + content: { + text: (statusText || "").trim(), + html: (statusText || "").trim(), + }, + author: { + name: profile?.name || handle, + url: actorUri.href, + photo: profile?.icon || "", + handle: `@${handle}@${hostname}`, + }, + published: now, + createdAt: now, + inReplyTo: inReplyTo || null, + category: [], + counts: { likes: 0, boosts: 0, replies: 0 }, + }; + + try { + await addTimelineItem(collections.ap_timeline, timelineItem); + } catch (storeError) { + console.warn("[Mastodon API] Failed to store outbound DM in timeline:", storeError.message); + } + + // Return a full serialized status so clients (Phanpy, Elk) can render it + const status = serializeStatus(timelineItem, { + baseUrl, + favouritedIds: new Set(), + rebloggedIds: new Set(), + bookmarkedIds: new Set(), + pinnedIds: new Set(), }); + return res.json(status); } // ── End DM path ───────────────────────────────────────────────────────────