Unverified Commit 6dd359fd authored by Hong Minhee's avatar Hong Minhee
Browse files

Sending activities

parent 7f39bf58
Loading
Loading
Loading
Loading
+18 −1
Original line number Diff line number Diff line
import { Federation } from "fedify/federation/middleware.ts";
import { fetchDocumentLoader, kvCache } from "fedify/runtime/docloader.ts";
import { isActor } from "fedify/vocab/actor.ts";
import {
  Accept,
  Activity,
  Create,
  CryptographicKey,
@@ -100,7 +102,22 @@ federation.setOutboxDispatcher(

federation.setInboxListeners("/users/{handle}/inbox")
  .on(Follow, async (ctx, follow) => {
    console.log({ follow });
    const blog = await getBlog();
    if (blog == null) return;
    const actorUri = ctx.getActorUri(blog.handle);
    if (follow.objectId?.href != actorUri.href) {
      return;
    }
    const recipient = await follow.getActor(ctx);
    if (!isActor(recipient)) return;
    await ctx.sendActivity(
      { keyId: new URL(`${actorUri}#main-key`), privateKey: blog.privateKey },
      recipient,
      new Accept({
        actor: actorUri,
        object: follow,
      }),
    );
  })
  .on(Undo, async (ctx, undo) => {
    console.log({ undo });
+3 −3
Original line number Diff line number Diff line
@@ -25,7 +25,7 @@ export async function setBlog(blog: BlogInput): Promise<void> {
      name: "RSASSA-PKCS1-v1_5",
      modulusLength: 4096,
      publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
      hash: "SHA-512",
      hash: "SHA-256",
    },
    true,
    ["sign", "verify"],
@@ -55,14 +55,14 @@ export async function getBlog(): Promise<Blog | null> {
    privateKey: await crypto.subtle.importKey(
      "jwk",
      entry.value.privateKey,
      { name: "RSASSA-PKCS1-v1_5", hash: "SHA-512" },
      { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
      true,
      ["sign"],
    ),
    publicKey: await crypto.subtle.importKey(
      "jwk",
      entry.value.publicKey,
      { name: "RSASSA-PKCS1-v1_5", hash: "SHA-512" },
      { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
      true,
      ["verify"],
    ),
+43 −0
Original line number Diff line number Diff line
import { validateCryptoKey } from "../httpsig/key.ts";
import { DocumentLoader } from "../runtime/docloader.ts";
import { Actor } from "../vocab/actor.ts";
import { Activity } from "../vocab/mod.ts";
import { Router, RouterError } from "./router.ts";
import { extractInboxes, sendActivity } from "./send.ts";

/**
 * A context for a request.
@@ -6,6 +11,11 @@ import { Router, RouterError } from "./router.ts";
export class Context<TContextData> {
  #router: Router;

  /**
   * The document loader used for loading remote JSON-LD documents.
   */
  readonly documentLoader: DocumentLoader;

  /**
   * The request object.
   */
@@ -24,17 +34,20 @@ export class Context<TContextData> {
  /**
   * Create a new context.
   * @param router The router used for the request.
   * @param documentLoader: The document loader used for JSON-LD context retrieval.
   * @param request The request object.
   * @param data The user-defined data associated with the context.
   * @param treatHttps Whether to treat the request as HTTPS even if it's not.
   */
  constructor(
    router: Router,
    documentLoader: DocumentLoader,
    request: Request,
    data: TContextData,
    treatHttps = false,
  ) {
    this.#router = router;
    this.documentLoader = documentLoader;
    this.request = request;
    this.data = data;
    this.url = new URL(request.url);
@@ -79,4 +92,34 @@ export class Context<TContextData> {
    }
    return new URL(path, this.url);
  }

  /**
   * Sends an activity to recipients' inboxes.
   * @param sender The sender's handle or sender's key pair.
   * @param recipients The recipients of the activity.
   * @param activity The activity to send.
   * @param options Options for sending the activity.
   */
  async sendActivity(
    sender: { keyId: URL; privateKey: CryptoKey },
    recipients: Actor | Actor[],
    activity: Activity,
    { preferSharedInbox }: { preferSharedInbox?: boolean } = {},
  ): Promise<void> {
    const { keyId, privateKey } = sender;
    validateCryptoKey(privateKey, "private");
    const inboxes = extractInboxes({
      recipients: Array.isArray(recipients) ? recipients : [recipients],
      preferSharedInbox,
    });
    for (const inbox of inboxes) {
      const successful = await sendActivity({
        keyId,
        privateKey,
        activity,
        inbox,
        documentLoader: this.documentLoader,
      });
    }
  }
}
+1 −0
Original line number Diff line number Diff line
@@ -182,6 +182,7 @@ export class Federation<TContextData> {
    }
    const context = new Context(
      this.#router,
      this.#documentLoader,
      request,
      contextData,
      this.#treatHttps,

federation/send.ts

0 → 100644
+103 −0
Original line number Diff line number Diff line
import { sign } from "../httpsig/mod.ts";
import { DocumentLoader } from "../runtime/docloader.ts";
import { Actor } from "../vocab/actor.ts";
import { Activity } from "../vocab/mod.ts";

/**
 * Parameters for {@link extractInboxes}.
 */
export interface ExtractInboxesParameters {
  /**
   * Actors to extract the inboxes from.
   */
  recipients: Actor[];

  /**
   * Whether to prefer the shared inbox over the personal inbox.
   * Defaults to `false`.
   */
  preferSharedInbox?: boolean;
}

/**
 * Extracts the inbox URLs from recipients.
 * @param parameters The parameters to extract the inboxes.
 *                   See also {@link ExtractInboxesParameters}.
 * @returns The inbox URLs.
 */
export function extractInboxes(
  { recipients, preferSharedInbox }: ExtractInboxesParameters,
): Set<URL> {
  const inboxes = new Set<URL>();
  for (const recipient of recipients) {
    let inbox = preferSharedInbox
      ? recipient.endpoints?.sharedInbox ?? recipient.inboxId
      : recipient.inboxId;
    if (inbox != null) inboxes.add(inbox);
  }
  return inboxes;
}

/**
 * Parameters for {@link sendActivity}.
 */
export interface SendActivityParameters {
  /**
   * The activity to send.
   */
  activity: Activity;

  /**
   * The actor's private key to sign the request.
   */
  privateKey: CryptoKey;

  /**
   * The public key ID that corresponds to the private key.
   */
  keyId: URL;

  /**
   * The inbox URL to send the activity to.
   */
  inbox: URL;

  /**
   * The document loader to use for JSON-LD context retrieval.
   */
  documentLoader?: DocumentLoader;
}

/**
 * Sends an {@link Activity} to an inbox.
 *
 * @param parameters The parameters for sending the activity.
 *                   See also {@link SendActivityParameters}.
 * @returns Whether the activity was successfully sent.
 */
export async function sendActivity(
  {
    activity,
    privateKey,
    keyId,
    inbox,
    documentLoader,
  }: SendActivityParameters,
): Promise<boolean> {
  if (activity.actorId == null) {
    throw new TypeError(
      "The activity to send must have at least one actor property.",
    );
  }
  const jsonLd = await activity.toJsonLd({ documentLoader });
  let request = new Request(inbox, {
    method: "POST",
    headers: {
      "Content-Type": "application/ld+json",
    },
    body: JSON.stringify(jsonLd),
  });
  request = await sign(request, privateKey, keyId);
  const response = await fetch(request);
  return response.ok;
}
Loading