diff --git a/index.js b/index.js index ec2bfde..963a5d8 100644 --- a/index.js +++ b/index.js @@ -179,7 +179,7 @@ export default class ActivityPubEndpoint { router.get("/admin/following", followingController(mp)); router.get("/admin/activities", activitiesController(mp)); router.get("/admin/migrate", migrateGetController(mp, this.options)); - router.post("/admin/migrate", migratePostController(mp, this.options)); + router.post("/admin/migrate", express.urlencoded({ extended: true, limit: "5mb" }), migratePostController(mp, this.options)); return router; } diff --git a/lib/controllers/migrate.js b/lib/controllers/migrate.js index 6b02b48..1677279 100644 --- a/lib/controllers/migrate.js +++ b/lib/controllers/migrate.js @@ -63,11 +63,12 @@ export function migratePostController(mountPath, pluginOptions) { text: response.locals.__("activitypub.migrate.errorNoFile"), }; } else { - let followingResult = { imported: 0, failed: 0 }; - let followersResult = { imported: 0, failed: 0 }; + let followingResult = { imported: 0, failed: 0, errors: [] }; + let followersResult = { imported: 0, failed: 0, errors: [] }; if (importFollowing && followingCollection) { const handles = parseMastodonFollowingCsv(fileContent); + console.log(`[ActivityPub] Migration: parsed ${handles.length} following handles from CSV`); followingResult = await bulkImportFollowing( handles, followingCollection, @@ -76,6 +77,7 @@ export function migratePostController(mountPath, pluginOptions) { if (importFollowers && followersCollection) { const entries = parseMastodonFollowersList(fileContent); + console.log(`[ActivityPub] Migration: parsed ${entries.length} follower entries from CSV`); followersResult = await bulkImportFollowers( entries, followersCollection, @@ -84,13 +86,28 @@ export function migratePostController(mountPath, pluginOptions) { const totalFailed = followingResult.failed + followersResult.failed; + const allErrors = [ + ...followingResult.errors, + ...followersResult.errors, + ]; + + let text = response.locals + .__("activitypub.migrate.success") + .replace("%d", followingResult.imported) + .replace("%d", followersResult.imported) + .replace("%d", totalFailed); + + if (allErrors.length > 0) { + text += " " + response.locals + .__("activitypub.migrate.failedList") + .replace("%s", allErrors.join(", ")); + } + result = { - type: "success", - text: response.locals - .__("activitypub.migrate.success") - .replace("%d", followingResult.imported) - .replace("%d", followersResult.imported) - .replace("%d", totalFailed), + type: totalFailed > 0 && followingResult.imported + followersResult.imported === 0 + ? "error" + : "success", + text, }; } } diff --git a/lib/migration.js b/lib/migration.js index 11ccdea..cd3a4f7 100644 --- a/lib/migration.js +++ b/lib/migration.js @@ -54,7 +54,10 @@ export function parseMastodonFollowersList(text) { */ export async function resolveHandleViaWebFinger(handle) { const [user, domain] = handle.split("@"); - if (!user || !domain) return null; + if (!user || !domain) { + console.warn(`[ActivityPub] Migration: invalid handle "${handle}" — skipping`); + return null; + } try { // WebFinger lookup @@ -64,14 +67,20 @@ export async function resolveHandleViaWebFinger(handle) { signal: AbortSignal.timeout(10_000), }); - if (!wfResponse.ok) return null; + if (!wfResponse.ok) { + console.warn(`[ActivityPub] Migration: WebFinger failed for ${handle} (HTTP ${wfResponse.status})`); + return null; + } const jrd = await wfResponse.json(); const selfLink = jrd.links?.find( (l) => l.rel === "self" && l.type === "application/activity+json", ); - if (!selfLink?.href) return null; + if (!selfLink?.href) { + console.warn(`[ActivityPub] Migration: no ActivityPub self link for ${handle}`); + return null; + } // Fetch actor document for inbox and profile const actorResponse = await fetch(selfLink.href, { @@ -79,7 +88,10 @@ export async function resolveHandleViaWebFinger(handle) { signal: AbortSignal.timeout(10_000), }); - if (!actorResponse.ok) return null; + if (!actorResponse.ok) { + console.warn(`[ActivityPub] Migration: actor fetch failed for ${handle} (HTTP ${actorResponse.status})`); + return null; + } const actor = await actorResponse.json(); return { @@ -89,7 +101,8 @@ export async function resolveHandleViaWebFinger(handle) { name: actor.name || actor.preferredUsername || handle, handle: actor.preferredUsername || user, }; - } catch { + } catch (error) { + console.warn(`[ActivityPub] Migration: resolve failed for ${handle}: ${error.message}`); return null; } } @@ -99,16 +112,23 @@ export async function resolveHandleViaWebFinger(handle) { * * @param {string[]} handles - Array of handles to import * @param {Collection} collection - MongoDB ap_following collection - * @returns {Promise<{imported: number, failed: number}>} + * @returns {Promise<{imported: number, failed: number, errors: string[]}>} */ export async function bulkImportFollowing(handles, collection) { let imported = 0; let failed = 0; + const errors = []; + + console.log(`[ActivityPub] Migration: importing ${handles.length} following entries...`); + + for (let i = 0; i < handles.length; i++) { + const handle = handles[i]; + console.log(`[ActivityPub] Migration: resolving following ${i + 1}/${handles.length}: ${handle}`); - for (const handle of handles) { const resolved = await resolveHandleViaWebFinger(handle); if (!resolved) { failed++; + errors.push(handle); continue; } @@ -130,7 +150,12 @@ export async function bulkImportFollowing(handles, collection) { imported++; } - return { imported, failed }; + console.log(`[ActivityPub] Migration: following import complete — ${imported} imported, ${failed} failed`); + if (errors.length > 0) { + console.log(`[ActivityPub] Migration: failed handles: ${errors.join(", ")}`); + } + + return { imported, failed, errors }; } /** @@ -140,15 +165,24 @@ export async function bulkImportFollowing(handles, collection) { * * @param {string[]} entries - Array of handles or actor URLs * @param {Collection} collection - MongoDB ap_followers collection - * @returns {Promise<{imported: number, failed: number}>} + * @returns {Promise<{imported: number, failed: number, errors: string[]}>} */ export async function bulkImportFollowers(entries, collection) { let imported = 0; let failed = 0; + const errors = []; - for (const entry of entries) { + console.log(`[ActivityPub] Migration: importing ${entries.length} follower entries...`); + + for (let i = 0; i < entries.length; i++) { + const entry = entries[i]; // If it's a URL, store directly; if it's a handle, resolve via WebFinger const isUrl = entry.startsWith("http"); + + if (!isUrl) { + console.log(`[ActivityPub] Migration: resolving follower ${i + 1}/${entries.length}: ${entry}`); + } + let actorData; if (isUrl) { @@ -159,6 +193,7 @@ export async function bulkImportFollowers(entries, collection) { if (!actorData) { failed++; + errors.push(entry); continue; } @@ -180,5 +215,10 @@ export async function bulkImportFollowers(entries, collection) { imported++; } - return { imported, failed }; + console.log(`[ActivityPub] Migration: follower import complete — ${imported} imported, ${failed} failed`); + if (errors.length > 0) { + console.log(`[ActivityPub] Migration: failed entries: ${errors.join(", ")}`); + } + + return { imported, failed, errors }; } diff --git a/locales/en.json b/locales/en.json index ef3cb70..dc96b12 100644 --- a/locales/en.json +++ b/locales/en.json @@ -43,6 +43,7 @@ "step3Desc": "Once you have saved your alias and imported your data, go to your Mastodon instance → Preferences → Account → Move to a different account. Enter your new fediverse handle and confirm. Mastodon will notify all your followers, and those whose servers support it will automatically re-follow you here. This step is irreversible — your old account will become a redirect.", "errorNoFile": "Please select a CSV file before importing.", "success": "Imported %d following, %d followers (%d failed).", + "failedList": "Could not resolve: %s", "aliasSuccess": "Alias saved — your actor document now includes this account as alsoKnownAs." } } diff --git a/package.json b/package.json index 090e464..a180233 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@rmdes/indiekit-endpoint-activitypub", - "version": "0.1.7", + "version": "0.1.8", "description": "ActivityPub federation endpoint for Indiekit via Fedify. Adds full fediverse support: actor, inbox, outbox, followers, following, syndication, and Mastodon migration.", "keywords": [ "indiekit",