diff --git a/index.js b/index.js index a380b9e..9d5ea27 100644 --- a/index.js +++ b/index.js @@ -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; diff --git a/lib/federation-setup.js b/lib/federation-setup.js index 2be054b..965aac4 100644 --- a/lib/federation-setup.js +++ b/lib/federation-setup.js @@ -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} + */ +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. diff --git a/package-lock.json b/package-lock.json index c364a8f..e6ba53b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index fb377bf..221e696 100644 --- a/package.json +++ b/package.json @@ -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",