fix: use .objectId accessors to prevent fetch errors from remote servers

Inbox handlers used await activity.getObject() which HTTP-fetches remote
objects. This fails when remote servers have Authorized Fetch enabled or
are unavailable, causing Fedify to retry ~10 times per activity.

Replaced with .objectId/.actorId accessors (zero network requests) for
Like, Announce, Undo, and Delete handlers. Wrapped remaining getObject()
and getActor() calls in try-catch with fallback to ID accessors.

Also adds Pinned Posts and Featured Tags cards to the admin dashboard.
This commit is contained in:
Ricardo
2026-02-20 23:44:18 +01:00
parent 4827a98614
commit 71851eba9b
4 changed files with 75 additions and 24 deletions
+11
View File
@@ -12,6 +12,9 @@ export function dashboardController(mountPath) {
const followingCollection = application?.collections?.get("ap_following"); const followingCollection = application?.collections?.get("ap_following");
const activitiesCollection = const activitiesCollection =
application?.collections?.get("ap_activities"); application?.collections?.get("ap_activities");
const featuredCollection = application?.collections?.get("ap_featured");
const featuredTagsCollection =
application?.collections?.get("ap_featured_tags");
const followerCount = followersCollection const followerCount = followersCollection
? await followersCollection.countDocuments() ? await followersCollection.countDocuments()
@@ -19,6 +22,12 @@ export function dashboardController(mountPath) {
const followingCount = followingCollection const followingCount = followingCollection
? await followingCollection.countDocuments() ? await followingCollection.countDocuments()
: 0; : 0;
const pinnedCount = featuredCollection
? await featuredCollection.countDocuments()
: 0;
const tagCount = featuredTagsCollection
? await featuredTagsCollection.countDocuments()
: 0;
const recentActivities = activitiesCollection const recentActivities = activitiesCollection
? await activitiesCollection ? await activitiesCollection
@@ -38,6 +47,8 @@ export function dashboardController(mountPath) {
title: response.locals.__("activitypub.title"), title: response.locals.__("activitypub.title"),
followerCount, followerCount,
followingCount, followingCount,
pinnedCount,
tagCount,
recentActivities, recentActivities,
refollowStatus, refollowStatus,
mountPath, mountPath,
+47 -15
View File
@@ -85,9 +85,14 @@ export function registerInboxListeners(inboxChain, options) {
}); });
}) })
.on(Undo, async (ctx, undo) => { .on(Undo, async (ctx, undo) => {
const actorObj = await undo.getActor(); const actorUrl = undo.actorId?.href || "";
const actorUrl = actorObj?.id?.href || ""; let inner;
const inner = await undo.getObject(); try {
inner = await undo.getObject();
} catch {
// Inner activity not dereferenceable — can't determine what was undone
return;
}
if (inner instanceof Follow) { if (inner instanceof Follow) {
await collections.ap_followers.deleteOne({ actorUrl }); await collections.ap_followers.deleteOne({ actorUrl });
@@ -98,14 +103,14 @@ export function registerInboxListeners(inboxChain, options) {
summary: `${actorUrl} unfollowed you`, summary: `${actorUrl} unfollowed you`,
}); });
} else if (inner instanceof Like) { } else if (inner instanceof Like) {
const objectId = (await inner.getObject())?.id?.href || ""; const objectId = inner.objectId?.href || "";
await collections.ap_activities.deleteOne({ await collections.ap_activities.deleteOne({
type: "Like", type: "Like",
actorUrl, actorUrl,
objectUrl: objectId, objectUrl: objectId,
}); });
} else if (inner instanceof Announce) { } else if (inner instanceof Announce) {
const objectId = (await inner.getObject())?.id?.href || ""; const objectId = inner.objectId?.href || "";
await collections.ap_activities.deleteOne({ await collections.ap_activities.deleteOne({
type: "Announce", type: "Announce",
actorUrl, actorUrl,
@@ -194,18 +199,27 @@ export function registerInboxListeners(inboxChain, options) {
} }
}) })
.on(Like, async (ctx, like) => { .on(Like, async (ctx, like) => {
const objectId = (await like.getObject())?.id?.href || ""; // Use .objectId to get the URL without dereferencing the remote object.
// Calling .getObject() would trigger an HTTP fetch to the remote server,
// which fails with 404 when the server has Authorized Fetch (Secure Mode)
// enabled — causing pointless retries and log spam.
const objectId = like.objectId?.href || "";
// Only log likes of our own content // Only log likes of our own content
const pubUrl = collections._publicationUrl; const pubUrl = collections._publicationUrl;
if (!objectId || (pubUrl && !objectId.startsWith(pubUrl))) return; if (!objectId || (pubUrl && !objectId.startsWith(pubUrl))) return;
const actorUrl = like.actorId?.href || "";
let actorName = actorUrl;
try {
const actorObj = await like.getActor(); const actorObj = await like.getActor();
const actorUrl = actorObj?.id?.href || ""; actorName =
const actorName =
actorObj?.name?.toString() || actorObj?.name?.toString() ||
actorObj?.preferredUsername?.toString() || actorObj?.preferredUsername?.toString() ||
actorUrl; actorUrl;
} catch {
/* actor not dereferenceable — use URL */
}
await logActivity(collections, storeRawActivities, { await logActivity(collections, storeRawActivities, {
direction: "inbound", direction: "inbound",
@@ -217,18 +231,24 @@ export function registerInboxListeners(inboxChain, options) {
}); });
}) })
.on(Announce, async (ctx, announce) => { .on(Announce, async (ctx, announce) => {
const objectId = (await announce.getObject())?.id?.href || ""; // Use .objectId — no remote fetch needed (see Like handler comment)
const objectId = announce.objectId?.href || "";
// Only log boosts of our own content // Only log boosts of our own content
const pubUrl = collections._publicationUrl; const pubUrl = collections._publicationUrl;
if (!objectId || (pubUrl && !objectId.startsWith(pubUrl))) return; if (!objectId || (pubUrl && !objectId.startsWith(pubUrl))) return;
const actorUrl = announce.actorId?.href || "";
let actorName = actorUrl;
try {
const actorObj = await announce.getActor(); const actorObj = await announce.getActor();
const actorUrl = actorObj?.id?.href || ""; actorName =
const actorName =
actorObj?.name?.toString() || actorObj?.name?.toString() ||
actorObj?.preferredUsername?.toString() || actorObj?.preferredUsername?.toString() ||
actorUrl; actorUrl;
} catch {
/* actor not dereferenceable — use URL */
}
await logActivity(collections, storeRawActivities, { await logActivity(collections, storeRawActivities, {
direction: "inbound", direction: "inbound",
@@ -240,11 +260,23 @@ export function registerInboxListeners(inboxChain, options) {
}); });
}) })
.on(Create, async (ctx, create) => { .on(Create, async (ctx, create) => {
const object = await create.getObject(); let object;
try {
object = await create.getObject();
} catch {
// Remote object not dereferenceable (Authorized Fetch, deleted, etc.)
return;
}
if (!object) return; if (!object) return;
const actorObj = await create.getActor(); const actorUrl = create.actorId?.href || "";
const actorUrl = actorObj?.id?.href || ""; let actorObj;
try {
actorObj = await create.getActor();
} catch {
// Actor not dereferenceable — use URL as fallback
actorObj = null;
}
const actorName = const actorName =
actorObj?.name?.toString() || actorObj?.name?.toString() ||
actorObj?.preferredUsername?.toString() || actorObj?.preferredUsername?.toString() ||
@@ -284,7 +316,7 @@ export function registerInboxListeners(inboxChain, options) {
}); });
}) })
.on(Delete, async (ctx, del) => { .on(Delete, async (ctx, del) => {
const objectId = (await del.getObject())?.id?.href || ""; const objectId = del.objectId?.href || "";
if (objectId) { if (objectId) {
await collections.ap_activities.deleteMany({ objectUrl: objectId }); await collections.ap_activities.deleteMany({ objectUrl: objectId });
} }
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@rmdes/indiekit-endpoint-activitypub", "name": "@rmdes/indiekit-endpoint-activitypub",
"version": "1.0.26", "version": "1.0.27",
"description": "ActivityPub federation endpoint for Indiekit via Fedify. Adds full fediverse support: actor, inbox, outbox, followers, following, syndication, and Mastodon migration.", "description": "ActivityPub federation endpoint for Indiekit via Fedify. Adds full fediverse support: actor, inbox, outbox, followers, following, syndication, and Mastodon migration.",
"keywords": [ "keywords": [
"indiekit", "indiekit",
+8
View File
@@ -22,6 +22,14 @@
title: __("activitypub.activities"), title: __("activitypub.activities"),
url: mountPath + "/admin/activities" url: mountPath + "/admin/activities"
}, },
{
title: pinnedCount + " " + __("activitypub.featured"),
url: mountPath + "/admin/featured"
},
{
title: tagCount + " " + __("activitypub.featuredTags"),
url: mountPath + "/admin/tags"
},
{ {
title: __("activitypub.profile.title"), title: __("activitypub.profile.title"),
url: mountPath + "/admin/profile" url: mountPath + "/admin/profile"