@fedify/fedify
Version:
An ActivityPub server framework
169 lines (168 loc) • 6.28 kB
JavaScript
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}`;
}