mirror of
https://github.com/svemagie/indiekit-endpoint-activitypub.git
synced 2026-04-02 15:44:58 +02:00
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:
@@ -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) : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user