From a259c79a31ed7f818d1c052ed2aef4c17dc31a07 Mon Sep 17 00:00:00 2001 From: svemagie <869694+svemagie@users.noreply.github.com> Date: Mon, 23 Mar 2026 08:30:14 +0100 Subject: [PATCH] fix(mastodon-api): favourite/like 404 for items with BSON Date or timezone-offset published MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three-layer fix for findTimelineItemById cursor mismatches: 1. encodeCursor returns "" (falsy) for invalid dates — serializeStatus now correctly falls back to item._id.toString() instead of using "0" as an opaque ID that can never be looked up. 2. findTimelineItemById adds two new fallbacks after the existing string lookups fail: - BSON Date lookup: Micropub pipeline (postData.create) may store published as a JavaScript Date → MongoDB BSON Date; string comparison never matches. - ±999 ms ISO range query: some AP servers send published with a timezone offset (e.g. +01:00). String(Temporal.Instant) preserves the original offset; decodeCursor normalizes to UTC, so the stored string and the lookup string differ. 3. timeline-store.js extractObjectData now normalizes published via new Date(String(...)).toISOString() before storing, ensuring all future items are stored in UTC ISO format and the exact-match lookup succeeds without needing the range fallback. Co-Authored-By: Claude Sonnet 4.6 --- lib/mastodon/helpers/pagination.js | 4 ++-- lib/mastodon/routes/statuses.js | 20 +++++++++++++++++++- lib/timeline-store.js | 6 ++++-- 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/lib/mastodon/helpers/pagination.js b/lib/mastodon/helpers/pagination.js index 3f4da71..98ff388 100644 --- a/lib/mastodon/helpers/pagination.js +++ b/lib/mastodon/helpers/pagination.js @@ -27,9 +27,9 @@ const MAX_LIMIT = 40; * @returns {string} Numeric string (ms since epoch) */ export function encodeCursor(published) { - if (!published) return "0"; + if (!published) return ""; const ms = new Date(published).getTime(); - return Number.isFinite(ms) ? String(ms) : "0"; + return Number.isFinite(ms) && ms > 0 ? String(ms) : ""; } /** diff --git a/lib/mastodon/routes/statuses.js b/lib/mastodon/routes/statuses.js index 0aa119f..6a34367 100644 --- a/lib/mastodon/routes/statuses.js +++ b/lib/mastodon/routes/statuses.js @@ -733,7 +733,7 @@ async function findTimelineItemById(collection, id) { // Try cursor-based lookup first (published date from ms-since-epoch) const publishedDate = decodeCursor(id); if (publishedDate) { - // Try exact match first (with .000Z suffix from toISOString) + // Try exact UTC ISO match (e.g., "2026-03-21T15:33:50.000Z") let item = await collection.findOne({ published: publishedDate }); if (item) return item; @@ -744,6 +744,24 @@ async function findTimelineItemById(collection, id) { item = await collection.findOne({ published: withoutMs }); if (item) return item; } + + // Try BSON Date (Micropub pipeline stores published as Date objects) + item = await collection.findOne({ published: new Date(publishedDate) }); + if (item) return item; + + // Try date-range lookup for timezone-offset stored strings (+01:00 etc.) + // Some AP servers emit non-UTC dates; decodeCursor normalizes to UTC but + // the stored string may differ. Search a ±1 s window using a regex on the + // ms value, or simply try the raw numeric id as a direct uid/url lookup. + const ms = Number.parseInt(id, 10); + if (ms > 0) { + const lo = new Date(ms - 999).toISOString().replace(/\.999Z$/, "Z"); + const hi = new Date(ms + 999).toISOString().replace(/\.999Z$/, "Z"); + item = await collection.findOne({ + published: { $gte: lo, $lte: hi }, + }); + if (item) return item; + } } // Fall back to ObjectId lookup (legacy IDs) diff --git a/lib/timeline-store.js b/lib/timeline-store.js index 28b05bf..69ca29c 100644 --- a/lib/timeline-store.js +++ b/lib/timeline-store.js @@ -214,9 +214,11 @@ export async function extractObjectData(object, options = {}) { pollClosed = closedValue === true || (closedValue != null && closedValue !== false); } - // Published date — store as ISO string per Indiekit convention + // Published date — store as UTC ISO string so cursor-based lookups always match. + // String(Temporal.Instant) preserves the original timezone offset (e.g. +01:00); + // normalizing via new Date() converts to Z-suffix UTC, matching decodeCursor output. const published = object.published - ? String(object.published) + ? new Date(String(object.published)).toISOString() : new Date().toISOString(); // Edited date — non-null when the post has been updated after publishing