@fedify/fedify
Version:
An ActivityPub server framework
149 lines (145 loc) • 5.79 kB
JavaScript
import { Temporal } from "@js-temporal/polyfill";
import { URLPattern } from "urlpattern-polyfill";
globalThis.addEventListener = () => {};
import { deno_default } from "./docloader-C8mmLN-Y.js";
import { Application, Group, Organization, Person, Service } from "./vocab-dDpPQ0fF.js";
import { lookupWebFinger } from "./lookup-Ozuw-rxN.js";
import { getTypeId } from "./type-D2s5lmbZ.js";
import { SpanStatusCode, trace } from "@opentelemetry/api";
import { domainToASCII, domainToUnicode } from "node:url";
//#region vocab/actor.ts
/**
* 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}.
*/
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.
*/
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.
*/
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
*/
async function getActorHandle(actor, options = {}) {
const tracerProvider = options.tracerProvider ?? trace.getTracerProvider();
const tracer = tracerProvider.getTracer(deno_default.name, deno_default.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$1 of response.aliases ?? []) if (new URL(alias$1).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
*/
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}`;
}
//#endregion
export { getActorClassByTypeName, getActorHandle, getActorTypeName, isActor, normalizeActorHandle };