mirror of
https://github.com/svemagie/indiekit-endpoint-activitypub.git
synced 2026-04-02 15:44:58 +02:00
- Server-level blocking: O(1) Redis SISMEMBER check in all inbox listeners, admin UI for blocking/unblocking servers by hostname, MongoDB fallback - Redis caching for collection dispatchers: 300s TTL on followers/following/liked counters and paginated pages, one-shot followers recipients cache - Proactive key refresh: daily cron re-fetches actor documents for followers with 7+ day stale keys using lookupWithSecurity() - Async inbox processing: MongoDB-backed queue with 3s polling, retry (3 attempts), 24h TTL auto-prune. Follow keeps synchronous Accept, Block keeps synchronous follower removal. All other activity types fully deferred to background processor. Inspired by wafrn's battle-tested multi-user AP implementation. Confab-Link: http://localhost:8080/sessions/af5f8b45-6b8d-442d-8f25-78c326190709
139 lines
4.2 KiB
JavaScript
139 lines
4.2 KiB
JavaScript
/**
|
|
* Proactive key refresh for remote actors.
|
|
* Periodically re-fetches actor documents for active followers
|
|
* whose keys may have rotated, keeping Fedify's KV cache fresh.
|
|
* @module key-refresh
|
|
*/
|
|
|
|
import { lookupWithSecurity } from "./lookup-helpers.js";
|
|
|
|
/**
|
|
* Update key freshness tracking after successfully processing
|
|
* an activity from a remote actor.
|
|
* @param {object} collections - MongoDB collections
|
|
* @param {string} actorUrl - Remote actor URL
|
|
*/
|
|
export async function touchKeyFreshness(collections, actorUrl) {
|
|
if (!actorUrl || !collections.ap_key_freshness) return;
|
|
try {
|
|
await collections.ap_key_freshness.updateOne(
|
|
{ actorUrl },
|
|
{
|
|
$set: { lastSeenAt: new Date().toISOString() },
|
|
$setOnInsert: { lastRefreshedAt: new Date().toISOString() },
|
|
},
|
|
{ upsert: true },
|
|
);
|
|
} catch {
|
|
// Non-critical
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Refresh stale keys for active followers.
|
|
* Finds followers whose keys haven't been refreshed in 7+ days
|
|
* and re-fetches their actor documents (up to 10 per cycle).
|
|
*
|
|
* @param {object} collections - MongoDB collections
|
|
* @param {object} ctx - Fedify context (for lookupObject)
|
|
* @param {string} handle - Our actor handle
|
|
*/
|
|
export async function refreshStaleKeys(collections, ctx, handle) {
|
|
if (!collections.ap_key_freshness || !collections.ap_followers) return;
|
|
|
|
const sevenDaysAgo = new Date(Date.now() - 7 * 86_400_000).toISOString();
|
|
|
|
// Find actors with stale keys who are still our followers
|
|
const staleActors = await collections.ap_key_freshness
|
|
.aggregate([
|
|
{
|
|
$match: {
|
|
lastRefreshedAt: { $lt: sevenDaysAgo },
|
|
},
|
|
},
|
|
{
|
|
$lookup: {
|
|
from: "ap_followers",
|
|
localField: "actorUrl",
|
|
foreignField: "actorUrl",
|
|
as: "follower",
|
|
},
|
|
},
|
|
{ $match: { "follower.0": { $exists: true } } },
|
|
{ $limit: 10 },
|
|
])
|
|
.toArray();
|
|
|
|
if (staleActors.length === 0) return;
|
|
|
|
console.info(`[ActivityPub] Refreshing keys for ${staleActors.length} stale actors`);
|
|
|
|
const documentLoader = await ctx.getDocumentLoader({ identifier: handle });
|
|
|
|
for (const entry of staleActors) {
|
|
try {
|
|
const result = await lookupWithSecurity(ctx, new URL(entry.actorUrl), {
|
|
documentLoader,
|
|
});
|
|
|
|
await collections.ap_key_freshness.updateOne(
|
|
{ actorUrl: entry.actorUrl },
|
|
{ $set: { lastRefreshedAt: new Date().toISOString() } },
|
|
);
|
|
|
|
if (!result) {
|
|
// Actor gone — log as stale
|
|
await collections.ap_activities?.insertOne({
|
|
direction: "system",
|
|
type: "StaleActor",
|
|
actorUrl: entry.actorUrl,
|
|
summary: `Actor ${entry.actorUrl} could not be resolved during key refresh`,
|
|
receivedAt: new Date().toISOString(),
|
|
});
|
|
}
|
|
} catch (error) {
|
|
const status = error?.cause?.status || error?.message || "unknown";
|
|
if (status === 410 || String(status).includes("410")) {
|
|
// 410 Gone — actor deleted
|
|
await collections.ap_activities?.insertOne({
|
|
direction: "system",
|
|
type: "StaleActor",
|
|
actorUrl: entry.actorUrl,
|
|
summary: `Actor ${entry.actorUrl} returned 410 Gone during key refresh`,
|
|
receivedAt: new Date().toISOString(),
|
|
});
|
|
}
|
|
// Update lastRefreshedAt even on failure to avoid retrying every cycle
|
|
await collections.ap_key_freshness.updateOne(
|
|
{ actorUrl: entry.actorUrl },
|
|
{ $set: { lastRefreshedAt: new Date().toISOString() } },
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Schedule key refresh job (runs on startup + every 24h).
|
|
* @param {object} collections - MongoDB collections
|
|
* @param {Function} getCtx - Function returning a Fedify context
|
|
* @param {string} handle - Our actor handle
|
|
*/
|
|
export function scheduleKeyRefresh(collections, getCtx, handle) {
|
|
const run = async () => {
|
|
try {
|
|
const ctx = getCtx();
|
|
if (ctx) {
|
|
await refreshStaleKeys(collections, ctx, handle);
|
|
}
|
|
} catch (error) {
|
|
console.error("[ActivityPub] Key refresh failed:", error.message);
|
|
}
|
|
};
|
|
|
|
// Run once on startup (delayed to let federation initialize)
|
|
setTimeout(run, 30_000);
|
|
|
|
// Then every 24 hours
|
|
setInterval(run, 86_400_000);
|
|
}
|