UNPKG

@fedify/fedify

Version:

An ActivityPub server framework

169 lines (168 loc) • 6.28 kB
import { SpanStatusCode, trace } from "@opentelemetry/api"; import { domainToASCII, domainToUnicode } from "node:url"; import metadata from "../deno.js"; import { lookupWebFinger } from "../webfinger/lookup.js"; import { getTypeId } from "./type.js"; import { Application, Group, Organization, Person, Service } from "./vocab.js"; /** * Checks if the given object is an {@link Actor}. * @param object The object to check. * @returns `true` if the given object is an {@link Actor}. */ export function isActor(object) { return (object instanceof Application || object instanceof Group || object instanceof Organization || object instanceof Person || object instanceof Service); } /** * Gets the type name of the given actor. * @param actor The actor to get the type name of. * @returns The type name of the given actor. */ export function getActorTypeName(actor) { if (actor instanceof Application) return "Application"; else if (actor instanceof Group) return "Group"; else if (actor instanceof Organization) return "Organization"; else if (actor instanceof Person) return "Person"; else if (actor instanceof Service) return "Service"; throw new Error("Unknown actor type."); } /** * Gets the actor class by the given type name. * @param typeName The type name to get the actor class by. * @returns The actor class by the given type name. */ export function getActorClassByTypeName(typeName) { switch (typeName) { case "Application": return Application; case "Group": return Group; case "Organization": return Organization; case "Person": return Person; case "Service": return Service; } throw new Error("Unknown actor type name."); } /** * Gets the actor handle, of the form `@username@domain`, from the given actor * or an actor URI. * * @example * ``` typescript * // Get the handle of an actor object: * await getActorHandle( * new Person({ id: new URL("https://fosstodon.org/users/hongminhee") }) * ); * * // Get the handle of an actor URI: * await getActorHandle(new URL("https://fosstodon.org/users/hongminhee")); * ``` * * @param actor The actor or actor URI to get the handle from. * @param options The extra options for getting the actor handle. * @returns The actor handle. It starts with `@` and is followed by the * username and domain, separated by `@` by default (it can be * customized with the options). * @throws {TypeError} If the actor does not have enough information to get the * handle. * @since 0.4.0 */ export async function getActorHandle(actor, options = {}) { const tracerProvider = options.tracerProvider ?? trace.getTracerProvider(); const tracer = tracerProvider.getTracer(metadata.name, metadata.version); return await tracer.startActiveSpan("activitypub.get_actor_handle", async (span) => { if (isActor(actor)) { if (actor.id != null) { span.setAttribute("activitypub.actor.id", actor.id.href); } span.setAttribute("activitypub.actor.type", getTypeId(actor).href); } try { return await getActorHandleInternal(actor, options); } catch (error) { span.setStatus({ code: SpanStatusCode.ERROR, message: String(error), }); throw error; } finally { span.end(); } }); } async function getActorHandleInternal(actor, options = {}) { const actorId = actor instanceof URL ? actor : actor.id; if (actorId != null) { const result = await lookupWebFinger(actorId, { userAgent: options.userAgent, tracerProvider: options.tracerProvider, }); if (result != null) { const aliases = [...(result.aliases ?? [])]; if (result.subject != null) aliases.unshift(result.subject); for (const alias of aliases) { const match = alias.match(/^acct:([^@]+)@([^@]+)$/); if (match != null) { const hostname = new URL(`https://${match[2]}/`).hostname; if (hostname !== actorId.hostname && !await verifyCrossOriginActorHandle(actorId.href, alias, options.userAgent, options.tracerProvider)) { continue; } return normalizeActorHandle(`@${match[1]}@${match[2]}`, options); } } } } if (!(actor instanceof URL) && actor.preferredUsername != null && actor.id != null) { return normalizeActorHandle(`@${actor.preferredUsername}@${actor.id.host}`, options); } throw new TypeError("Actor does not have enough information to get the handle."); } async function verifyCrossOriginActorHandle(actorId, alias, userAgent, tracerProvider) { const response = await lookupWebFinger(alias, { userAgent, tracerProvider }); if (response == null) return false; for (const alias of response.aliases ?? []) { if (new URL(alias).href === actorId) return true; } return false; } /** * Normalizes the given actor handle. * @param handle The full handle of the actor to normalize. * @param options The options for normalizing the actor handle. * @returns The normalized actor handle. * @throws {TypeError} If the actor handle is invalid. * @since 0.9.0 */ export function normalizeActorHandle(handle, options = {}) { handle = handle.replace(/^@/, ""); const atPos = handle.indexOf("@"); if (atPos < 1) throw new TypeError("Invalid actor handle."); let domain = handle.substring(atPos + 1); if (domain.length < 1 || domain.includes("@")) { throw new TypeError("Invalid actor handle."); } domain = domain.toLowerCase(); domain = options.punycode ? domainToASCII(domain) : domainToUnicode(domain); domain = domain.toLowerCase(); const user = handle.substring(0, atPos); return options.trimLeadingAt ? `${user}@${domain}` : `@${user}@${domain}`; }