Files
indiekit-endpoint-activitypub/lib/controllers/resolve.js
Ricardo 9a61145d97 feat: FEP-8fcf/fe34 compliance, custom emoji, manual follow approval (v2.13.0)
- FEP-8fcf: add syncCollection to Undo(Announce) sendActivity
- FEP-fe34: centralized lookupWithSecurity() helper with crossOrigin: "ignore" on all 23 lookupObject call sites
- Custom emoji: replaceCustomEmoji() renders :shortcode: as inline <img> in content and actor display names
- Manual follow approval: profile toggle, ap_pending_follows collection, approve/reject controllers with federation, pending tab on followers page, follow_request notification type
- Coverage audit updated to v2.12.x (overall ~70% → ~82%)

Confab-Link: http://localhost:8080/sessions/1f1e729b-0087-499e-a991-f36f46211fe4
2026-03-17 08:21:36 +01:00

111 lines
3.0 KiB
JavaScript

/**
* Resolve controller — accepts any fediverse URL or handle, resolves it
* via lookupObject(), and redirects to the appropriate internal view.
*/
import { lookupWithSecurity } from "../lookup-helpers.js";
import {
Article,
Note,
Person,
Service,
Application,
Organization,
Group,
} from "@fedify/fedify/vocab";
/**
* GET /admin/reader/resolve?q=<url-or-handle>
* Resolves a fediverse URL or @user@domain handle and redirects to
* the post detail or remote profile view.
*/
export function resolveController(mountPath, plugin) {
return async (request, response, next) => {
try {
const query = (request.query.q || "").trim();
if (!query) {
return response.redirect(`${mountPath}/admin/reader`);
}
if (!plugin._federation) {
return response.status(503).render("error", {
title: "Error",
content: "Federation not initialized",
});
}
const handle = plugin.options.actor.handle;
const ctx = plugin._federation.createContext(
new URL(plugin._publicationUrl),
{ handle, publicationUrl: plugin._publicationUrl },
);
const documentLoader = await ctx.getDocumentLoader({
identifier: handle,
});
// Determine if input is a URL or a handle
// lookupObject accepts: URLs, @user@domain, user@domain, acct:user@domain
let lookupInput;
try {
// If it parses as a URL, pass as URL object
const parsed = new URL(query);
lookupInput = parsed;
} catch {
// Not a URL — treat as handle (strip leading @ if present)
lookupInput = query;
}
let object;
try {
object = await lookupWithSecurity(ctx,lookupInput, { documentLoader });
} catch (error) {
console.warn(
`[resolve] lookupObject failed for "${query}":`,
error.message,
);
}
if (!object) {
return response.status(404).render("error", {
title: response.locals.__("activitypub.reader.resolve.notFoundTitle"),
content: response.locals.__(
"activitypub.reader.resolve.notFound",
),
});
}
// Determine object type and redirect accordingly
const objectUrl =
object.id?.href || object.url?.href || query;
if (
object instanceof Person ||
object instanceof Service ||
object instanceof Application ||
object instanceof Organization ||
object instanceof Group
) {
return response.redirect(
`${mountPath}/admin/reader/profile?url=${encodeURIComponent(objectUrl)}`,
);
}
if (object instanceof Note || object instanceof Article) {
return response.redirect(
`${mountPath}/admin/reader/post?url=${encodeURIComponent(objectUrl)}`,
);
}
// Unknown type — try post detail as fallback
return response.redirect(
`${mountPath}/admin/reader/post?url=${encodeURIComponent(objectUrl)}`,
);
} catch (error) {
next(error);
}
};
}