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 express from "express";
|
||||||
|
|
||||||
import { setupFederation } from "./lib/federation-setup.js";
|
import { setupFederation, buildPersonActor } from "./lib/federation-setup.js";
|
||||||
import {
|
import {
|
||||||
createFedifyMiddleware,
|
createFedifyMiddleware,
|
||||||
} from "./lib/federation-bridge.js";
|
} from "./lib/federation-bridge.js";
|
||||||
@@ -690,9 +690,15 @@ export default class ActivityPubEndpoint {
|
|||||||
{ handle, publicationUrl: this._publicationUrl },
|
{ handle, publicationUrl: this._publicationUrl },
|
||||||
);
|
);
|
||||||
|
|
||||||
// Retrieve the full actor from the dispatcher (same object remote
|
// Build the full actor object (same as what the dispatcher serves).
|
||||||
// servers will get when they re-fetch the actor URL)
|
// Note: ctx.getActor() only exists on RequestContext, not the base
|
||||||
const actor = await ctx.getActor(handle);
|
// Context returned by createContext(), so we use the shared helper.
|
||||||
|
const actor = await buildPersonActor(
|
||||||
|
ctx,
|
||||||
|
handle,
|
||||||
|
this._collections,
|
||||||
|
this.options.actorType,
|
||||||
|
);
|
||||||
if (!actor) {
|
if (!actor) {
|
||||||
console.warn("[ActivityPub] broadcastActorUpdate: could not build actor");
|
console.warn("[ActivityPub] broadcastActorUpdate: could not build actor");
|
||||||
return;
|
return;
|
||||||
|
|||||||
+85
-68
@@ -142,74 +142,7 @@ export function setupFederation(options) {
|
|||||||
|
|
||||||
if (identifier !== handle) return null;
|
if (identifier !== handle) return null;
|
||||||
|
|
||||||
const profile = await getProfile(collections);
|
return buildPersonActor(ctx, identifier, collections, actorType);
|
||||||
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);
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.mapHandle((_ctx, username) => {
|
.mapHandle((_ctx, username) => {
|
||||||
@@ -678,6 +611,90 @@ async function getProfile(collections) {
|
|||||||
return doc || {};
|
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.
|
* 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.
|
* 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",
|
"name": "@rmdes/indiekit-endpoint-activitypub",
|
||||||
"version": "1.1.13",
|
"version": "1.1.17",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@rmdes/indiekit-endpoint-activitypub",
|
"name": "@rmdes/indiekit-endpoint-activitypub",
|
||||||
"version": "1.1.13",
|
"version": "1.1.17",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fedify/express": "^1.10.3",
|
"@fedify/express": "^1.10.3",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@rmdes/indiekit-endpoint-activitypub",
|
"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.",
|
"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",
|
||||||
|
|||||||
Reference in New Issue
Block a user