fix: hashtag timeline returns empty results
- inReplyTo stored as empty string "" for non-replies in timeline-store.js;
filters using {$exists: false} missed these docs → hashtag/public timelines empty
- Fix: omit inReplyTo/quoteUrl fields entirely when empty (undefined spread)
- Fix: update all {$exists: false} filters to also match {inReplyTo: ""}
- Fix: hashtag timeline query uses case-insensitive prefix regex so
"politics" matches "on/politics", "wandern" matches stored "wandern"
- DB migration: unset inReplyTo:"" on 620 existing ap_timeline documents
This commit is contained in:
@@ -439,7 +439,7 @@ router.get("/api/v1/accounts/:id/statuses", tokenRequired, scopeRequired("read",
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
if (req.query.exclude_replies === "true") {
|
if (req.query.exclude_replies === "true") {
|
||||||
baseFilter.inReplyTo = { $exists: false };
|
baseFilter.$or = [{ inReplyTo: { $exists: false } }, { inReplyTo: "" }];
|
||||||
}
|
}
|
||||||
if (req.query.exclude_reblogs === "true") {
|
if (req.query.exclude_reblogs === "true") {
|
||||||
baseFilter.type = { $ne: "boost" };
|
baseFilter.type = { $ne: "boost" };
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ router.get("/api/v1/timelines/public", async (req, res, next) => {
|
|||||||
// Public timeline: only public visibility, no context items, no replies
|
// Public timeline: only public visibility, no context items, no replies
|
||||||
const baseFilter = {
|
const baseFilter = {
|
||||||
isContext: { $ne: true },
|
isContext: { $ne: true },
|
||||||
inReplyTo: { $exists: false },
|
$or: [{ inReplyTo: { $exists: false } }, { inReplyTo: "" }],
|
||||||
visibility: "public",
|
visibility: "public",
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -220,11 +220,18 @@ router.get("/api/v1/timelines/tag/:hashtag", async (req, res, next) => {
|
|||||||
const limit = parseLimit(req.query.limit);
|
const limit = parseLimit(req.query.limit);
|
||||||
const hashtag = req.params.hashtag;
|
const hashtag = req.params.hashtag;
|
||||||
|
|
||||||
|
// Match the hashtag case-insensitively and as a prefix segment
|
||||||
|
// (e.g. "politics" matches "on/politics", "politics/foo").
|
||||||
|
// Escape to prevent regex injection — hashtag comes from a URL param.
|
||||||
|
const escapedHashtag = hashtag.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||||
|
const tagRegex = new RegExp(`(^|/)${escapedHashtag}(/|$)`, "i");
|
||||||
|
|
||||||
const baseFilter = {
|
const baseFilter = {
|
||||||
isContext: { $ne: true },
|
isContext: { $ne: true },
|
||||||
inReplyTo: { $exists: false },
|
// inReplyTo is absent on non-replies; legacy docs may store "" — exclude both
|
||||||
|
$or: [{ inReplyTo: { $exists: false } }, { inReplyTo: "" }],
|
||||||
visibility: { $in: ["public", "unlisted"] },
|
visibility: { $in: ["public", "unlisted"] },
|
||||||
category: hashtag,
|
category: tagRegex,
|
||||||
};
|
};
|
||||||
|
|
||||||
const { filter, sort, reverse } = buildPaginationQuery(baseFilter, {
|
const { filter, sort, reverse } = buildPaginationQuery(baseFilter, {
|
||||||
|
|||||||
@@ -348,11 +348,12 @@ export async function extractObjectData(object, options = {}) {
|
|||||||
// Attachment extraction failed — non-critical
|
// Attachment extraction failed — non-critical
|
||||||
}
|
}
|
||||||
|
|
||||||
// In-reply-to — Fedify uses replyTargetId (non-fetching)
|
// In-reply-to — Fedify uses replyTargetId (non-fetching).
|
||||||
const inReplyTo = object.replyTargetId?.href || "";
|
// Only set when non-empty — $exists:false filters depend on field absence.
|
||||||
|
const inReplyTo = object.replyTargetId?.href || undefined;
|
||||||
|
|
||||||
// Quote URL — Fedify reads quoteUrl / _misskey_quote / quoteUri
|
// Quote URL — Fedify reads quoteUrl / _misskey_quote / quoteUri
|
||||||
const quoteUrl = object.quoteUrl?.href || "";
|
const quoteUrl = object.quoteUrl?.href || undefined;
|
||||||
|
|
||||||
// Interaction counts — not fetched at ingest time. The three collection
|
// Interaction counts — not fetched at ingest time. The three collection
|
||||||
// fetches (getReplies, getLikes, getShares) each trigger an HTTP round-trip
|
// fetches (getReplies, getLikes, getShares) each trigger an HTTP round-trip
|
||||||
@@ -382,8 +383,8 @@ export async function extractObjectData(object, options = {}) {
|
|||||||
photo,
|
photo,
|
||||||
video,
|
video,
|
||||||
audio,
|
audio,
|
||||||
inReplyTo,
|
...(inReplyTo ? { inReplyTo } : {}),
|
||||||
quoteUrl,
|
...(quoteUrl ? { quoteUrl } : {}),
|
||||||
counts,
|
counts,
|
||||||
pollOptions,
|
pollOptions,
|
||||||
votersCount,
|
votersCount,
|
||||||
|
|||||||
Reference in New Issue
Block a user