fix(mastodon-api): favourite fails for pre-UTC-normalization timeline items

findTimelineItemById's string range query used $gte/$lte on ISO strings,
which fails for items stored with timezone offsets (e.g. "+01:00"): the
string "2026-03-21T16:33:50+01:00" is lexicographically outside the
range ["2026-03-21T15:33:50Z", "2026-03-21T15:33:51Z"].

Replace with a $or that covers both cases:
- BSON Date stored (Micropub): direct Date range comparison
- String stored with any timezone: $dateFromString parses the offset
  correctly, $toLong converts to ms-since-epoch, numeric range always
  matches regardless of timezone format

Items received before extractObjectData's UTC normalization (a259c79)
was deployed are stored with the original server's timezone offset and
now resolve correctly through this fallback.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
svemagie
2026-03-23 10:53:20 +01:00
parent b33932f1f6
commit 2660a1a604

View File

@@ -749,16 +749,36 @@ async function findTimelineItemById(collection, id) {
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.
// Try ±1 s range lookup for timezone-offset stored strings (+01:00 etc.)
// and BSON Date fields. The UTC-ISO string range query used above fails when
// the stored value has a non-UTC timezone — "2026-03-21T16:33:50+01:00" is
// lexicographically outside ["2026-03-21T15:33:50Z", "2026-03-21T15:33:51Z"].
// $dateFromString parses any ISO 8601 format (including offsets) to a Date,
// $toLong converts it to ms-since-epoch, and the numeric range always matches.
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");
const lo = new Date(ms - 999);
const hi = new Date(ms + 999);
item = await collection.findOne({
published: { $gte: lo, $lte: hi },
$or: [
// BSON Date stored (Micropub pipeline) — direct Date range comparison
{ published: { $gte: lo, $lte: hi } },
// String stored with any timezone format — parse via $dateFromString
{
$expr: {
$and: [
{ $gte: [
{ $toLong: { $dateFromString: { dateString: "$published", onError: 0, onNull: 0 } } },
ms - 999,
] },
{ $lte: [
{ $toLong: { $dateFromString: { dateString: "$published", onError: 0, onNull: 0 } } },
ms + 999,
] },
],
},
},
],
});
if (item) return item;
}