fix: broadcastActorUpdate was silently failing due to Fedify API mismatch
ctx.getActor() only exists on RequestContext (inside HTTP handlers), not on the base Context returned by createContext(). Extracted actor-building logic into shared buildPersonActor() helper used by both the dispatcher and broadcastActorUpdate(). Profile link attachments now propagate to remote instances via Update(Person) activity.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import express from "express";
|
||||
|
||||
import { setupFederation } from "./lib/federation-setup.js";
|
||||
import { setupFederation, buildPersonActor } from "./lib/federation-setup.js";
|
||||
import {
|
||||
createFedifyMiddleware,
|
||||
} from "./lib/federation-bridge.js";
|
||||
@@ -690,9 +690,15 @@ export default class ActivityPubEndpoint {
|
||||
{ handle, publicationUrl: this._publicationUrl },
|
||||
);
|
||||
|
||||
// Retrieve the full actor from the dispatcher (same object remote
|
||||
// servers will get when they re-fetch the actor URL)
|
||||
const actor = await ctx.getActor(handle);
|
||||
// Build the full actor object (same as what the dispatcher serves).
|
||||
// Note: ctx.getActor() only exists on RequestContext, not the base
|
||||
// Context returned by createContext(), so we use the shared helper.
|
||||
const actor = await buildPersonActor(
|
||||
ctx,
|
||||
handle,
|
||||
this._collections,
|
||||
this.options.actorType,
|
||||
);
|
||||
if (!actor) {
|
||||
console.warn("[ActivityPub] broadcastActorUpdate: could not build actor");
|
||||
return;
|
||||
|
||||
+85
-68
@@ -142,74 +142,7 @@ export function setupFederation(options) {
|
||||
|
||||
if (identifier !== handle) return null;
|
||||
|
||||
const profile = await getProfile(collections);
|
||||
const keyPairs = await ctx.getActorKeyPairs(identifier);
|
||||
|
||||
const personOptions = {
|
||||
id: ctx.getActorUri(identifier),
|
||||
preferredUsername: identifier,
|
||||
name: profile.name || identifier,
|
||||
url: profile.url ? new URL(profile.url) : null,
|
||||
inbox: ctx.getInboxUri(identifier),
|
||||
outbox: ctx.getOutboxUri(identifier),
|
||||
followers: ctx.getFollowersUri(identifier),
|
||||
following: ctx.getFollowingUri(identifier),
|
||||
liked: ctx.getLikedUri(identifier),
|
||||
featured: ctx.getFeaturedUri(identifier),
|
||||
featuredTags: ctx.getFeaturedTagsUri(identifier),
|
||||
endpoints: new Endpoints({ sharedInbox: ctx.getInboxUri() }),
|
||||
manuallyApprovesFollowers:
|
||||
profile.manuallyApprovesFollowers || false,
|
||||
};
|
||||
|
||||
if (profile.summary) {
|
||||
personOptions.summary = profile.summary;
|
||||
}
|
||||
|
||||
if (profile.icon) {
|
||||
personOptions.icon = new Image({
|
||||
url: new URL(profile.icon),
|
||||
mediaType: guessImageMediaType(profile.icon),
|
||||
});
|
||||
}
|
||||
|
||||
if (profile.image) {
|
||||
personOptions.image = new Image({
|
||||
url: new URL(profile.image),
|
||||
mediaType: guessImageMediaType(profile.image),
|
||||
});
|
||||
}
|
||||
|
||||
if (keyPairs.length > 0) {
|
||||
personOptions.publicKey = keyPairs[0].cryptographicKey;
|
||||
personOptions.assertionMethods = keyPairs.map((k) => k.multikey);
|
||||
}
|
||||
|
||||
if (profile.attachments?.length > 0) {
|
||||
personOptions.attachments = profile.attachments.map(
|
||||
(att) =>
|
||||
new PropertyValue({
|
||||
name: att.name,
|
||||
value: formatAttachmentValue(att.value),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (profile.alsoKnownAs?.length > 0) {
|
||||
personOptions.alsoKnownAs = profile.alsoKnownAs.map(
|
||||
(u) => new URL(u),
|
||||
);
|
||||
}
|
||||
|
||||
if (profile.createdAt) {
|
||||
personOptions.published = Temporal.Instant.from(profile.createdAt);
|
||||
}
|
||||
|
||||
// Actor type from profile overrides config default
|
||||
const profileActorType = profile.actorType || actorType;
|
||||
const ResolvedActorClass = actorTypeMap[profileActorType] || ActorClass;
|
||||
|
||||
return new ResolvedActorClass(personOptions);
|
||||
return buildPersonActor(ctx, identifier, collections, actorType);
|
||||
},
|
||||
)
|
||||
.mapHandle((_ctx, username) => {
|
||||
@@ -678,6 +611,90 @@ async function getProfile(collections) {
|
||||
return doc || {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the Person/Service/Organization actor object from the stored profile.
|
||||
* Used by both the actor dispatcher (for serving the actor to federation
|
||||
* requests) and broadcastActorUpdate() (for sending Update activities).
|
||||
*
|
||||
* @param {object} ctx - Fedify context (base Context or RequestContext)
|
||||
* @param {string} identifier - Actor handle (e.g. "rick")
|
||||
* @param {object} collections - MongoDB collections
|
||||
* @param {string} [defaultActorType="Person"] - Fallback actor type
|
||||
* @returns {Promise<import("@fedify/fedify").Actor|null>}
|
||||
*/
|
||||
export async function buildPersonActor(
|
||||
ctx,
|
||||
identifier,
|
||||
collections,
|
||||
defaultActorType = "Person",
|
||||
) {
|
||||
const actorTypeMap = { Person, Service, Application, Organization, Group };
|
||||
const profile = await getProfile(collections);
|
||||
const keyPairs = await ctx.getActorKeyPairs(identifier);
|
||||
|
||||
const personOptions = {
|
||||
id: ctx.getActorUri(identifier),
|
||||
preferredUsername: identifier,
|
||||
name: profile.name || identifier,
|
||||
url: profile.url ? new URL(profile.url) : null,
|
||||
inbox: ctx.getInboxUri(identifier),
|
||||
outbox: ctx.getOutboxUri(identifier),
|
||||
followers: ctx.getFollowersUri(identifier),
|
||||
following: ctx.getFollowingUri(identifier),
|
||||
liked: ctx.getLikedUri(identifier),
|
||||
featured: ctx.getFeaturedUri(identifier),
|
||||
featuredTags: ctx.getFeaturedTagsUri(identifier),
|
||||
endpoints: new Endpoints({ sharedInbox: ctx.getInboxUri() }),
|
||||
manuallyApprovesFollowers: profile.manuallyApprovesFollowers || false,
|
||||
};
|
||||
|
||||
if (profile.summary) {
|
||||
personOptions.summary = profile.summary;
|
||||
}
|
||||
|
||||
if (profile.icon) {
|
||||
personOptions.icon = new Image({
|
||||
url: new URL(profile.icon),
|
||||
mediaType: guessImageMediaType(profile.icon),
|
||||
});
|
||||
}
|
||||
|
||||
if (profile.image) {
|
||||
personOptions.image = new Image({
|
||||
url: new URL(profile.image),
|
||||
mediaType: guessImageMediaType(profile.image),
|
||||
});
|
||||
}
|
||||
|
||||
if (keyPairs.length > 0) {
|
||||
personOptions.publicKey = keyPairs[0].cryptographicKey;
|
||||
personOptions.assertionMethods = keyPairs.map((k) => k.multikey);
|
||||
}
|
||||
|
||||
if (profile.attachments?.length > 0) {
|
||||
personOptions.attachments = profile.attachments.map(
|
||||
(att) =>
|
||||
new PropertyValue({
|
||||
name: att.name,
|
||||
value: formatAttachmentValue(att.value),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (profile.alsoKnownAs?.length > 0) {
|
||||
personOptions.alsoKnownAs = profile.alsoKnownAs.map((u) => new URL(u));
|
||||
}
|
||||
|
||||
if (profile.createdAt) {
|
||||
personOptions.published = Temporal.Instant.from(profile.createdAt);
|
||||
}
|
||||
|
||||
const profileActorType = profile.actorType || defaultActorType;
|
||||
const ResolvedActorClass = actorTypeMap[profileActorType] || Person;
|
||||
|
||||
return new ResolvedActorClass(personOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Import a PKCS#8 PEM private key using Web Crypto API.
|
||||
* Fedify's importPem only handles PKCS#1, but Node.js crypto generates PKCS#8.
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@rmdes/indiekit-endpoint-activitypub",
|
||||
"version": "1.1.13",
|
||||
"version": "1.1.17",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@rmdes/indiekit-endpoint-activitypub",
|
||||
"version": "1.1.13",
|
||||
"version": "1.1.17",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@fedify/express": "^1.10.3",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@rmdes/indiekit-endpoint-activitypub",
|
||||
"version": "1.1.17",
|
||||
"version": "1.1.18",
|
||||
"description": "ActivityPub federation endpoint for Indiekit via Fedify. Adds full fediverse support: actor, inbox, outbox, followers, following, syndication, and Mastodon migration.",
|
||||
"keywords": [
|
||||
"indiekit",
|
||||
|
||||
Reference in New Issue
Block a user