Unverified Commit 42b64481 authored by Hong Minhee's avatar Hong Minhee
Browse files

normalizeActorHandle() function

parent 308523df
Loading
Loading
Loading
Loading
+11 −0
Original line number Diff line number Diff line
@@ -12,6 +12,17 @@ To be released.

 -  Added `Emoji` class to Activity Vocabulary API.  [[#48]]

 -  Added an actor handle normalization function.

     -  Added `normalizeActorHandle()` function.
     -  Added `NormalizeActorHandleOptions` interface.
     -  The `getActorHandle()` function now guarantees that the returned
        actor handle is normalized.
     -  Added the second optional parameter to `getActorHandle()` function.
     -  The return type of `getActorHandle()` function became
        ``Promise<`@${string}@${string}` | `${string}@${string}`>``
        (was ``Promise<`@${string}@${string}`>``).

 -  Added more log messages using the [LogTape] library.  Currently the below
    logger categories are used:

+88 −0
Original line number Diff line number Diff line
@@ -4,6 +4,7 @@ import {
  assertFalse,
  assertRejects,
  assertStrictEquals,
  assertThrows,
} from "@std/assert";
import * as fc from "fast-check";
import * as mf from "mock_fetch";
@@ -13,6 +14,7 @@ import {
  getActorHandle,
  getActorTypeName,
  isActor,
  normalizeActorHandle,
} from "./actor.ts";
import { Application, Group, Organization, Person, Service } from "./vocab.ts";

@@ -110,7 +112,15 @@ Deno.test("getActorHandle()", async (t) => {

  await t.step("WebFinger subject", async () => {
    assertEquals(await getActorHandle(actor), "@john@example.com");
    assertEquals(
      await getActorHandle(actor, { trimLeadingAt: true }),
      "john@example.com",
    );
    assertEquals(await getActorHandle(actorId), "@john@example.com");
    assertEquals(
      await getActorHandle(actorId, { trimLeadingAt: true }),
      "john@example.com",
    );
  });

  mf.mock(
@@ -127,7 +137,15 @@ Deno.test("getActorHandle()", async (t) => {

  await t.step("WebFinger aliases", async () => {
    assertEquals(await getActorHandle(actor), "@john@bar.example.com");
    assertEquals(
      await getActorHandle(actor, { trimLeadingAt: true }),
      "john@bar.example.com",
    );
    assertEquals(await getActorHandle(actorId), "@john@bar.example.com");
    assertEquals(
      await getActorHandle(actorId, { trimLeadingAt: true }),
      "john@bar.example.com",
    );
  });

  mf.mock(
@@ -142,3 +160,73 @@ Deno.test("getActorHandle()", async (t) => {

  mf.uninstall();
});

Deno.test("normalizeActorHandle()", () => {
  assertEquals(normalizeActorHandle("@foo@BAR.COM"), "@foo@bar.com");
  assertEquals(normalizeActorHandle("@BAZ@☃-⌘.com"), "@BAZ@☃-⌘.com");
  assertEquals(
    normalizeActorHandle("@qux@xn--maana-pta.com"),
    "@qux@mañana.com",
  );
  assertEquals(
    normalizeActorHandle("@quux@XN--MAANA-PTA.COM"),
    "@quux@mañana.com",
  );
  assertEquals(
    normalizeActorHandle("@quux@MAÑANA.COM"),
    "@quux@mañana.com",
  );

  assertEquals(
    normalizeActorHandle("@foo@BAR.COM", { trimLeadingAt: true }),
    "foo@bar.com",
  );
  assertEquals(
    normalizeActorHandle("@BAZ@☃-⌘.com", { trimLeadingAt: true }),
    "BAZ@☃-⌘.com",
  );
  assertEquals(
    normalizeActorHandle("@qux@xn--maana-pta.com", { trimLeadingAt: true }),
    "qux@mañana.com",
  );
  assertEquals(
    normalizeActorHandle("@quux@XN--MAANA-PTA.COM", { trimLeadingAt: true }),
    "quux@mañana.com",
  );
  assertEquals(
    normalizeActorHandle("@quux@MAÑANA.COM", { trimLeadingAt: true }),
    "quux@mañana.com",
  );

  assertEquals(
    normalizeActorHandle("@foo@BAR.COM", { punycode: true }),
    "@foo@bar.com",
  );
  assertEquals(
    normalizeActorHandle("@BAZ@☃-⌘.com", { punycode: true }),
    "@BAZ@xn----dqo34k.com",
  );
  assertEquals(
    normalizeActorHandle("@qux@xn--maana-pta.com", { punycode: true }),
    "@qux@xn--maana-pta.com",
  );
  assertEquals(
    normalizeActorHandle("@quux@XN--MAANA-PTA.COM", { punycode: true }),
    "@quux@xn--maana-pta.com",
  );
  assertEquals(
    normalizeActorHandle("@quux@MAÑANA.COM", { punycode: true }),
    "@quux@xn--maana-pta.com",
  );

  assertThrows(() => normalizeActorHandle(""));
  assertThrows(() => normalizeActorHandle("@"));
  assertThrows(() => normalizeActorHandle("foo"));
  assertThrows(() => normalizeActorHandle("@foo"));
  assertThrows(() => normalizeActorHandle("@@foo.com"));
  assertThrows(() => normalizeActorHandle("@foo@"));
  assertThrows(() => normalizeActorHandle("foo@bar.com@baz.com"));
  assertThrows(() => normalizeActorHandle("@foo@bar.com@baz.com"));
});

// cSpell: ignore maana
+56 −4
Original line number Diff line number Diff line
import { toASCII, toUnicode } from "node:punycode";
import { lookupWebFinger } from "../webfinger/lookup.ts";
import { Application, Group, Organization, Person, Service } from "./vocab.ts";

@@ -92,15 +93,18 @@ export function getActorClassByTypeName(
 * ```
 *
 * @param actor The actor or actor URI to get the handle from.
 * @param options The options for normalizing the actor handle.
 * @returns The actor handle.  It starts with `@` and is followed by the
 *          username and domain, separated by `@`.
 *          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: Actor | URL,
): Promise<`@${string}@${string}`> {
  options: NormalizeActorHandleOptions = {},
): Promise<`@${string}@${string}` | `${string}@${string}`> {
  const actorId = actor instanceof URL ? actor : actor.id;
  if (actorId != null) {
    const result = await lookupWebFinger(actorId);
@@ -109,7 +113,9 @@ export async function getActorHandle(
      if (result.subject != null) aliases.unshift(result.subject);
      for (const alias of aliases) {
        const match = alias.match(/^acct:([^@]+)@([^@]+)$/);
        if (match != null) return `@${match[1]}@${match[2]}`;
        if (match != null) {
          return normalizeActorHandle(`@${match[1]}@${match[2]}`, options);
        }
      }
    }
  }
@@ -117,13 +123,59 @@ export async function getActorHandle(
    !(actor instanceof URL) && actor.preferredUsername != null &&
    actor.id != null
  ) {
    return `@${actor.preferredUsername}@${actor.id.host}`;
    return normalizeActorHandle(
      `@${actor.preferredUsername}@${actor.id.host}`,
      options,
    );
  }
  throw new TypeError(
    "Actor does not have enough information to get the handle.",
  );
}

/**
 * Options for {@link normalizeActorHandle}.
 * @since 0.9.0
 */
export interface NormalizeActorHandleOptions {
  /**
   * Whether to trim the leading `@` from the actor handle.  Turned off by
   * default.
   */
  trimLeadingAt?: boolean;

  /**
   * Whether to convert the domain part of the actor handle to punycode, if it
   * is an internationalized domain name.  Turned off by default.
   */
  punycode?: boolean;
}

/**
 * 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.
 */
export function normalizeActorHandle(
  handle: string,
  options: NormalizeActorHandleOptions = {},
): `@${string}@${string}` | `${string}@${string}` {
  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 ? toASCII(domain) : toUnicode(domain);
  domain = domain.toLowerCase();
  const user = handle.substring(0, atPos);
  return options.trimLeadingAt ? `${user}@${domain}` : `@${user}@${domain}`;
}

/**
 * The object that can be a recipient of an activity.
 *