UNPKG

@fedify/fedify

Version:

An ActivityPub server framework

162 lines (158 loc) 5.6 kB
import { Temporal } from "@js-temporal/polyfill"; import { URLPattern } from "urlpattern-polyfill"; globalThis.addEventListener = () => {}; import { deno_default, getDocumentLoader } from "./docloader-C8mmLN-Y.js"; import { Object as Object$1 } from "./vocab-dDpPQ0fF.js"; import { lookupWebFinger } from "./lookup-Ozuw-rxN.js"; import { getTypeId } from "./type-D2s5lmbZ.js"; import { getLogger } from "@logtape/logtape"; import { SpanStatusCode, trace } from "@opentelemetry/api"; import { delay } from "@es-toolkit/es-toolkit"; //#region vocab/lookup.ts 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 */ async function lookupObject(identifier, options = {}) { const tracerProvider = options.tracerProvider ?? trace.getTracerProvider(); const tracer = tracerProvider.getTracer(deno_default.name, deno_default.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$1.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 */ async function* traverseCollection(collection, options = {}) { if (collection.firstId == null) for await (const item of collection.getItems(options)) yield item; else { const interval = 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); } } } //#endregion export { lookupObject, traverseCollection };