From 13d939c1467a34d6a8fb1e13d0c22e1e44c88cde Mon Sep 17 00:00:00 2001 From: Ricardo Date: Fri, 20 Feb 2026 09:40:20 +0100 Subject: [PATCH] fix: Accept(Follow) handler not matching incoming accepts Two issues prevented Accept activities from transitioning ap_following docs from refollow:sent to federation: 1. accept.getObject() often returns null because remote servers reference our outgoing Follow by URL, which Fedify can't resolve back. The strict instanceof Follow check caused early return on every Accept. Now we proceed to the MongoDB match if getObject() returns null or throws. 2. Batch processor sent Follow to entry.actorUrl but never updated the stored URL to the canonical form after resolving the remote actor. Now updates actorUrl to remoteActor.id.href so Accept handler matches. --- lib/batch-refollow.js | 25 +++++++++++++++++-------- lib/inbox-listeners.js | 19 +++++++++++++++++-- package.json | 2 +- 3 files changed, 35 insertions(+), 11 deletions(-) diff --git a/lib/batch-refollow.js b/lib/batch-refollow.js index 8a8e73a..b56fbae 100644 --- a/lib/batch-refollow.js +++ b/lib/batch-refollow.js @@ -233,23 +233,32 @@ async function processOneFollow(options, entry) { throw new Error("Could not resolve remote actor"); } - // Send Follow activity + // Use the canonical actor URL (may differ from imported URL) + const canonicalUrl = remoteActor.id?.href || entry.actorUrl; + + // Send Follow activity using canonical URL const follow = new Follow({ actor: ctx.getActorUri(handle), - object: new URL(entry.actorUrl), + object: new URL(canonicalUrl), }); await ctx.sendActivity({ identifier: handle }, remoteActor, follow); - // Mark as sent + // Mark as sent — update actorUrl to canonical form so Accept handler + // can match when the remote server responds + const updateFields = { + source: "refollow:sent", + refollowLastAttempt: new Date().toISOString(), + refollowError: null, + }; + if (canonicalUrl !== entry.actorUrl) { + updateFields.actorUrl = canonicalUrl; + } + await collections.ap_following.updateOne( { _id: entry._id }, { - $set: { - source: "refollow:sent", - refollowLastAttempt: new Date().toISOString(), - refollowError: null, - }, + $set: updateFields, $inc: { refollowAttempts: 1 }, }, ); diff --git a/lib/inbox-listeners.js b/lib/inbox-listeners.js index f6775f0..c63648b 100644 --- a/lib/inbox-listeners.js +++ b/lib/inbox-listeners.js @@ -125,8 +125,23 @@ export function registerInboxListeners(inboxChain, options) { const actorUrl = actorObj?.id?.href || ""; if (!actorUrl) return; - const inner = await accept.getObject(); - if (!(inner instanceof Follow)) return; + // Check if the inner object is a Follow. Some servers send the full + // Follow object, others send only a reference URL that Fedify can't + // resolve (since the original Follow was our outgoing activity). + let isFollow = false; + try { + const inner = await accept.getObject(); + isFollow = inner instanceof Follow; + // If inner resolved to a non-Follow activity, skip + if (inner && !isFollow) { + console.info( + `[ActivityPub] Accept from ${actorUrl}: inner is ${inner.constructor?.name}, not Follow — skipping`, + ); + return; + } + } catch { + // getObject() failed — proceed anyway if we have a pending follow + } // Match against our following list for refollow or microsub-reader follows const result = await collections.ap_following.findOneAndUpdate( diff --git a/package.json b/package.json index d0b2d76..bb74e9c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@rmdes/indiekit-endpoint-activitypub", - "version": "1.0.14", + "version": "1.0.15", "description": "ActivityPub federation endpoint for Indiekit via Fedify. Adds full fediverse support: actor, inbox, outbox, followers, following, syndication, and Mastodon migration.", "keywords": [ "indiekit",