Loading CHANGES.md +11 −0 Original line number Diff line number Diff line Loading @@ -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: Loading vocab/actor.test.ts +88 −0 Original line number Diff line number Diff line Loading @@ -4,6 +4,7 @@ import { assertFalse, assertRejects, assertStrictEquals, assertThrows, } from "@std/assert"; import * as fc from "fast-check"; import * as mf from "mock_fetch"; Loading @@ -13,6 +14,7 @@ import { getActorHandle, getActorTypeName, isActor, normalizeActorHandle, } from "./actor.ts"; import { Application, Group, Organization, Person, Service } from "./vocab.ts"; Loading Loading @@ -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( Loading @@ -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( Loading @@ -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 vocab/actor.ts +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"; Loading Loading @@ -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); Loading @@ -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); } } } } Loading @@ -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. * Loading Loading
CHANGES.md +11 −0 Original line number Diff line number Diff line Loading @@ -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: Loading
vocab/actor.test.ts +88 −0 Original line number Diff line number Diff line Loading @@ -4,6 +4,7 @@ import { assertFalse, assertRejects, assertStrictEquals, assertThrows, } from "@std/assert"; import * as fc from "fast-check"; import * as mf from "mock_fetch"; Loading @@ -13,6 +14,7 @@ import { getActorHandle, getActorTypeName, isActor, normalizeActorHandle, } from "./actor.ts"; import { Application, Group, Organization, Person, Service } from "./vocab.ts"; Loading Loading @@ -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( Loading @@ -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( Loading @@ -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
vocab/actor.ts +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"; Loading Loading @@ -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); Loading @@ -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); } } } } Loading @@ -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. * Loading