Files
svemagie 56a8b08498 fix: integrate all AP runtime patches into fork source
Integrates 12 runtime patch scripts from indiekit-server directly into
the fork source code, eliminating the need for postinstall patching:

- enrich-actor-data: avatar via getIcon(), handle as @user@domain, banner via getImage()
- conversations-endpoint: real /api/v1/conversations implementation
- stubs-remove-duplicate-routes: dead route removal from stubs.js
- self-follow-guard: prevent self-follow loop
- oauth-token-expiry: clear expiresAt on token exchange
- unify-dm-visibility: unified DM visibility detection
- accounts-id-cache-fallback: check ap_actor_cache before 404
- federation-infra: federation infrastructure fixes
- mastodon-misc: miscellaneous Mastodon API fixes
- mastodon-statuses: status endpoint fixes
- syndication: syndication dedup
- startup-gate-bypass: startup gate bypass

Also strips all // [patch] markers from 16 files (including 4 from prior commit).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-25 16:35:01 +02:00

116 lines
4.1 KiB
JavaScript

/**
* Timeline retention cleanup — removes old timeline items to prevent
* unbounded collection growth and cleans up stale interaction tracking.
*/
/**
* Remove timeline items beyond the retention limit and clean up
* corresponding ap_interactions entries.
*
* Uses aggregation to identify exact items to delete by UID,
* avoiding race conditions between finding and deleting.
*
* @param {object} collections - MongoDB collections
* @param {number} retentionLimit - Max number of timeline items to keep
* @returns {Promise<{removed: number, interactionsRemoved: number}>}
*/
export async function cleanupTimeline(collections, retentionLimit) {
if (!collections.ap_timeline || retentionLimit <= 0) {
return { removed: 0, interactionsRemoved: 0 };
}
// Get the local profile URL to exempt own posts from cleanup.
// Own posts are your content — they should never be deleted by retention.
const profile = collections.ap_profile
? await collections.ap_profile.findOne({})
: null;
const ownerUrl = profile?.url || null;
// Only count remote posts toward retention limit
const remoteFilter = ownerUrl
? { "author.url": { $ne: ownerUrl } }
: {};
const remoteCount = await collections.ap_timeline.countDocuments(remoteFilter);
if (remoteCount <= retentionLimit) {
return { removed: 0, interactionsRemoved: 0 };
}
// Find remote items beyond the retention limit, sorted newest-first.
// Own posts are excluded from the aggregation pipeline entirely.
const pipeline = [
...(ownerUrl ? [{ $match: { "author.url": { $ne: ownerUrl } } }] : []),
{ $sort: { published: -1 } },
{ $skip: retentionLimit },
{ $project: { uid: 1 } },
];
const toDelete = await collections.ap_timeline
.aggregate(pipeline)
.toArray();
if (!toDelete.length) {
return { removed: 0, interactionsRemoved: 0 };
}
const removedUids = toDelete.map((item) => item.uid).filter(Boolean);
// Preserve items the user has interacted with (liked, bookmarked, boosted).
// Deleting them would silently remove entries from the Favourites/Bookmarks pages.
let interactedUids = new Set();
if (removedUids.length > 0 && collections.ap_interactions) {
const interacted = await collections.ap_interactions.distinct("objectUrl");
interactedUids = new Set(interacted);
}
const itemsToDelete = toDelete.filter((item) => !interactedUids.has(item.uid));
const uidsToDelete = itemsToDelete.map((item) => item.uid).filter(Boolean);
if (!itemsToDelete.length) {
return { removed: 0, interactionsRemoved: 0 };
}
// Delete old timeline items by UID
const deleteResult = await collections.ap_timeline.deleteMany({
_id: { $in: itemsToDelete.map((item) => item._id) },
});
// Clean up stale interactions for removed items
let interactionsRemoved = 0;
if (uidsToDelete.length > 0 && collections.ap_interactions) {
const interactionResult = await collections.ap_interactions.deleteMany({
objectUrl: { $in: uidsToDelete },
});
interactionsRemoved = interactionResult.deletedCount || 0;
}
const removed = deleteResult.deletedCount || 0;
if (removed > 0) {
console.info(
`[ActivityPub] Timeline cleanup: removed ${removed} items, ${interactionsRemoved} stale interactions`,
);
}
return { removed, interactionsRemoved };
}
/**
* Schedule periodic timeline cleanup.
*
* @param {object} collections - MongoDB collections
* @param {number} retentionLimit - Max number of timeline items to keep
* @param {number} intervalMs - Cleanup interval in milliseconds (default: 24 hours)
* @returns {NodeJS.Timeout} The interval timer (for cleanup if needed)
*/
export function scheduleCleanup(collections, retentionLimit, intervalMs = 86_400_000) {
// Run immediately on startup
cleanupTimeline(collections, retentionLimit).catch((error) => {
console.error("[ActivityPub] Timeline cleanup failed:", error.message);
});
// Then run periodically
return setInterval(() => {
cleanupTimeline(collections, retentionLimit).catch((error) => {
console.error("[ActivityPub] Timeline cleanup failed:", error.message);
});
}, intervalMs);
}