fix(mastodon-api): favourite/like 404 for items with BSON Date or timezone-offset published

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 <noreply@anthropic.com>
This commit is contained in:
svemagie
2026-03-23 08:30:14 +01:00
parent 6c13eb85a5
commit a259c79a31
3 changed files with 25 additions and 5 deletions

View File

@@ -27,9 +27,9 @@ const MAX_LIMIT = 40;
* @returns {string} Numeric string (ms since epoch) * @returns {string} Numeric string (ms since epoch)
*/ */
export function encodeCursor(published) { export function encodeCursor(published) {
if (!published) return "0"; if (!published) return "";
const ms = new Date(published).getTime(); const ms = new Date(published).getTime();
return Number.isFinite(ms) ? String(ms) : "0"; return Number.isFinite(ms) && ms > 0 ? String(ms) : "";
} }
/** /**

View File

@@ -733,7 +733,7 @@ async function findTimelineItemById(collection, id) {
// Try cursor-based lookup first (published date from ms-since-epoch) // Try cursor-based lookup first (published date from ms-since-epoch)
const publishedDate = decodeCursor(id); const publishedDate = decodeCursor(id);
if (publishedDate) { 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 }); let item = await collection.findOne({ published: publishedDate });
if (item) return item; if (item) return item;
@@ -744,6 +744,24 @@ async function findTimelineItemById(collection, id) {
item = await collection.findOne({ published: withoutMs }); item = await collection.findOne({ published: withoutMs });
if (item) return item; 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) // Fall back to ObjectId lookup (legacy IDs)

View File

@@ -214,9 +214,11 @@ export async function extractObjectData(object, options = {}) {
pollClosed = closedValue === true || (closedValue != null && closedValue !== false); 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 const published = object.published
? String(object.published) ? new Date(String(object.published)).toISOString()
: new Date().toISOString(); : new Date().toISOString();
// Edited date — non-null when the post has been updated after publishing // Edited date — non-null when the post has been updated after publishing