@fedify/fedify
Version:
An ActivityPub server framework
175 lines (174 loc) • 6.36 kB
JavaScript
import * as dntShim from "../_dnt.shims.js";
import { getLogger } from "@logtape/logtape";
import { SpanStatusCode, trace } from "@opentelemetry/api";
import { delay } from "../deps/jsr.io/@std/async/1.0.12/delay.js";
import metadata from "../deno.js";
import { getDocumentLoader, } from "../runtime/docloader.js";
import { lookupWebFinger } from "../webfinger/lookup.js";
import { getTypeId } from "./type.js";
import { Object } from "./vocab.js";
const logger = getLogger(["fedify", "vocab", "lookup"]);
const handleRegexp = /^@?((?:[-A-Za-z0-9._~!$&'()*+,;=]|%[A-Fa-f0-9]{2})+)@([^@]+)$/;
/**
* Looks up an ActivityStreams object by its URI (including `acct:` URIs)
* or a fediverse handle (e.g., `@user@server` or `user@server`).
*
* @example
* ``` typescript
* // Look up an actor by its fediverse handle:
* await lookupObject("@hongminhee@fosstodon.org");
* // returning a `Person` object.
*
* // A fediverse handle can omit the leading '@':
* await lookupObject("hongminhee@fosstodon.org");
* // returning a `Person` object.
*
* // A `acct:` URI can be used as well:
* await lookupObject("acct:hongminhee@fosstodon.org");
* // returning a `Person` object.
*
* // Look up an object by its URI:
* await lookupObject("https://todon.eu/@hongminhee/112060633798771581");
* // returning a `Note` object.
*
* // It can be a `URL` object as well:
* await lookupObject(new URL("https://todon.eu/@hongminhee/112060633798771581"));
* // returning a `Note` object.
* ```
*
* @param identifier The URI or fediverse handle to look up.
* @param options Lookup options.
* @returns The object, or `null` if not found.
* @since 0.2.0
*/
export async function lookupObject(identifier, options = {}) {
const tracerProvider = options.tracerProvider ?? trace.getTracerProvider();
const tracer = tracerProvider.getTracer(metadata.name, metadata.version);
return await tracer.startActiveSpan("activitypub.lookup_object", async (span) => {
try {
const result = await lookupObjectInternal(identifier, options);
if (result == null)
span.setStatus({ code: SpanStatusCode.ERROR });
else {
if (result.id != null) {
span.setAttribute("activitypub.object.id", result.id.href);
}
span.setAttribute("activitypub.object.type", getTypeId(result).href);
if (result.replyTargetIds.length > 0) {
span.setAttribute("activitypub.object.in_reply_to", result.replyTargetIds.map((id) => id.href));
}
}
return result;
}
catch (error) {
span.setStatus({
code: SpanStatusCode.ERROR,
message: String(error),
});
throw error;
}
finally {
span.end();
}
});
}
async function lookupObjectInternal(identifier, options = {}) {
const documentLoader = options.documentLoader ??
getDocumentLoader({ userAgent: options.userAgent });
if (typeof identifier === "string") {
const match = handleRegexp.exec(identifier);
if (match)
identifier = `acct:${match[1]}@${match[2]}`;
identifier = new URL(identifier);
}
let document = null;
if (identifier.protocol === "http:" || identifier.protocol === "https:") {
try {
const remoteDoc = await documentLoader(identifier.href);
document = remoteDoc.document;
}
catch (error) {
logger.debug("Failed to fetch remote document:\n{error}", { error });
}
}
if (document == null) {
const jrd = await lookupWebFinger(identifier, {
userAgent: options.userAgent,
tracerProvider: options.tracerProvider,
allowPrivateAddress: "allowPrivateAddress" in options &&
options.allowPrivateAddress === true,
});
if (jrd?.links == null)
return null;
for (const l of jrd.links) {
if (l.type !== "application/activity+json" &&
!l.type?.match(/application\/ld\+json;\s*profile="https:\/\/www.w3.org\/ns\/activitystreams"/) || l.rel !== "self")
continue;
try {
const remoteDoc = await documentLoader(l.href);
document = remoteDoc.document;
break;
}
catch (error) {
logger.debug("Failed to fetch remote document:\n{error}", { error });
continue;
}
}
}
if (document == null)
return null;
try {
return await Object.fromJsonLd(document, {
documentLoader,
contextLoader: options.contextLoader,
tracerProvider: options.tracerProvider,
});
}
catch (error) {
if (error instanceof TypeError) {
logger.debug("Failed to parse JSON-LD document: {error}\n{document}", { error, document });
return null;
}
throw error;
}
}
/**
* Traverses a collection, yielding each item in the collection.
* If the collection is paginated, it will fetch the next page
* automatically.
*
* @example
* ``` typescript
* const collection = await lookupObject(collectionUrl);
* if (collection instanceof Collection) {
* for await (const item of traverseCollection(collection)) {
* console.log(item.id?.href);
* }
* }
* ```
*
* @param collection The collection to traverse.
* @param options Options for traversing the collection.
* @returns An async iterable of each item in the collection.
* @since 1.1.0
*/
export async function* traverseCollection(collection, options = {}) {
if (collection.firstId == null) {
for await (const item of collection.getItems(options)) {
yield item;
}
}
else {
const interval = dntShim.Temporal.Duration.from(options.interval ?? { seconds: 0 })
.total("millisecond");
let page = await collection.getFirst(options);
while (page != null) {
for await (const item of page.getItems(options)) {
yield item;
}
if (interval > 0)
await delay(interval);
page = await page.getNext(options);
}
}
}