Unverified Commit 0e336a3a authored by Hong Minhee's avatar Hong Minhee
Browse files

Merge pull request #278 from 2chanhaeng/create-webfinger-command

parents 553492a7 73ac3c53
Loading
Loading
Loading
Loading
+20 −0
Original line number Diff line number Diff line
@@ -31,6 +31,26 @@ To be released.
     -  Added `MemoryKvStore.cas()` method.
     -  Added `DenoKvStore.cas()` method.

 -  Added useful functions for fediverse handles at `@fedify/fedify/vocab`.
    This functions simplify working with fediverse handles and URLs.

     -  `FediverseHandle`: An interface representing a fediverse handle.
     -  `parseFediverseHandle()`: A function to parse a fediverse handle into
        its components.
     -  `isFediverseHandle()`: A function to check if a string is a valid
        fediverse handle.
     -  `toAcctUrl()`: A function to convert a fediverse handle to a `URL`.

 -  Added `fedify webfinger` command. This command allows users to look up
    WebFinger information for a given resource.

     -  The input can be a handle (e.g., `@user@server`, `user@server`) or
        a URL (e.g., `https://server/users/path`).
     -  The `--user-agent` or `-a` option used as `User-Agent` header value
        in the WebFinger request.
     -  The `--allow-private-address` or `-p` option allows looking up
        WebFinger information for private addresses (e.g., `localhost`).


Version 1.7.3
-------------
+2 −0
Original line number Diff line number Diff line
@@ -10,6 +10,7 @@ import { logFile, recordingSink } from "./log.ts";
import { command as lookup } from "./lookup.ts";
import { command as node } from "./node.ts";
import { command as tunnel } from "./tunnel.ts";
import { command as webfinger } from "./webfinger.ts";

const command = new Command()
  .name("fedify")
@@ -65,6 +66,7 @@ const command = new Command()
  .command("node", node)
  .command("tunnel", tunnel)
  .command("completions", new CompletionsCommand())
  .command("webfinger", webfinger)
  .command("help", new HelpCommand().global());

if (import.meta.main) {

cli/webfinger.ts

0 → 100644
+107 −0
Original line number Diff line number Diff line
import { Command } from "@cliffy/command";
import { toAcctUrl } from "@fedify/fedify/vocab";
import { lookupWebFinger } from "@fedify/fedify/webfinger";
import ora from "ora";
import { printJson } from "./utils.ts";

export const command = new Command()
  .arguments("<...resources:string>")
  .description(
    "Look up a WebFinger resource by resource. The argument can be multiple.",
  )
  .option(
    "-a, --user-agent <userAgent:string>",
    "The user agent to use for the request.",
  )
  .option(
    "-p, --allow-private-address",
    "Allow private IP addresses in the URL.",
  )
  .action(async (options, ...resources: string[]) => {
    for (const resource of resources) {
      const spinner = ora({ // Create a spinner for the lookup process
        text: `Looking up WebFinger for ${resource}`,
        discardStdin: false,
      }).start();
      try {
        const url = convertUrlIfHandle(resource); // Convert resource to URL
        const webFinger = await lookupWebFinger(url, options) ?? // Look up WebFinger
          new NotFoundError(resource).throw(); // throw NotFoundError if not found

        spinner.succeed(`WebFinger found for ${resource}:`); // Succeed the spinner
        printJson(webFinger); // Print the WebFinger
      } catch (error) {
        if (error instanceof InvalidHandleError) { // If the handle format is invalid,
          spinner.fail(`Invalid handle format: ${error.handle}`); // log error message with handle
        } else if (error instanceof NotFoundError) { // If the resource is not found,
          spinner.fail(`Resource not found: ${error.resource}`); // log not found message
        } else if (error instanceof Error) {
          spinner.fail( // For other errors, log the error message
            `Error looking up WebFinger for ${resource}: ${error}`,
          );
        }
      }
    }
  });

/**
 * Converts a handle or URL to a URL object.
 * If the input is a valid URL, it returns the URL object.
 * If the input is a handle in the format `@username@domain`, it converts it to a URL.
 * @param handleOrUrl The handle or URL to convert.
 * @returns A URL object representing the handle or URL.
 */
function convertUrlIfHandle(handleOrUrl: string): URL {
  try {
    return new URL(handleOrUrl); // Try to convert the input to a URL
  } catch {
    return convertHandleToUrl(handleOrUrl); // If it fails, treat it as a handle
  }
}

/**
 * Custom error class for invalid handle formats.
 * @param {string} handle The invalid handle that caused the error.
 * @extends {Error}
 */
class InvalidHandleError extends Error {
  constructor(public handle: string) {
    super(`Invalid handle format: ${handle}`);
    this.name = "InvalidHandleError";
  }
  throw(): never {
    throw this;
  }
}

/**
 * Custom error class for not found resources.
 * @param {string} resource The resource that was not found.
 * @extends {Error}
 */
class NotFoundError extends Error {
  constructor(public resource: string) {
    super(`Resource not found: ${resource}`);
    this.name = "NotFoundError";
  }
  throw(): never {
    throw this;
  }
}

/**
 * Converts a handle in the format `@username@domain` to a URL.
 * The resulting URL will be in the format `https://domain/@username`.
 * @param handle The handle to convert, in the format `@username@domain`.
 * @returns A URL object representing the handle.
 * @throws {Error} If the handle format is invalid.
 * @example
 * ```ts
 * const url = convertHandleToUrl("@username@domain.com");
 * console.log(url.toString()); // "https://domain.com/@username"
 * ```
 */
function convertHandleToUrl(handle: string): URL {
  return toAcctUrl(handle) ?? // Convert the handle to a URL
    new InvalidHandleError(handle).throw(); // or throw an error if invalid
}
+85 −0
Original line number Diff line number Diff line
@@ -1010,6 +1010,91 @@ command. For example, to use the serveo.net, run the below command:
fedify tunnel --service serveo.net 3000
~~~~

`fedify webfinger`: Looking up a WebFinger resource
---------------------------------------------------

*This command is available since Fedify 1.8.0.*

The `fedify webfinger` command is used to look up a WebFinger resource by
resource URI or handle.  [WebFinger] is a protocol that allows discovery of
information about people and other entities on the Internet using simple web
requests.  This command is useful for debugging and testing WebFinger
implementations.

To look up a WebFinger resource, for example, for a user handle, run the
below command:

~~~~ sh
fedify webfinger @username@domain.com
~~~~

The output will be like the below:

~~~~ json
{
  "subject": "acct:username@domain.com",
  "aliases": [
    "https://domain.com/@username",
    "https://domain.com/users/username"
  ],
  "links": [
    {
      "rel": "http://webfinger.net/rel/profile-page",
      "type": "text/html",
      "href": "https://domain.com/@username"
    },
    {
      "rel": "self",
      "type": "application/activity+json",
      "href": "https://domain.com/users/username"
    }
  ]
}
~~~~

You can also look up a WebFinger resource by its URL.  For example, the below
command looks up a WebFinger resource by http or acct URL:

~~~~ sh
fedify webfinger https://domain.com/@username
fedify webfinger acct:username@domain.com
~~~~

Or, you can also look up multiple WebFinger resources at once.  For example,
the below command looks up multiple WebFinger resources:

~~~~ sh
fedify webfinger @user1@domain.com https://domain.com/@username acct:username@domain.com
~~~~

The outputs will be displayed sequentially, each preceded by a success message
indicating which resource was found.

[WebFinger]: https://tools.ietf.org/html/rfc7033

### `-a`/`--user-agent`: Custom `User-Agent` header

By default, the `fedify webfinger` command sends the `User-Agent` header with
the value `Fedify/1.8.0 (Deno/2.4.0)` (version numbers may vary).  You can
specify a custom `User-Agent` header by using the `-a`/`--user-agent` option.
For example, to send the `User-Agent` header with the value `MyApp/1.0`, run
the below command:

~~~~ sh
fedify webfinger --user-agent MyApp/1.0 @username@domain.com
~~~~

### `-p`/`--allow-private-address`: Allow private IP addresses

The `-p`/`--allow-private-address` option is used to allow private IP addresses.
If you want to allow private IP addresses, run the below command:

~~~~ sh
fedify webfinger --allow-private-address @username@localhost
~~~~

Mostly useful for testing purposes.  *Do not use this in production.*


Shell completions
-----------------

fedify/vocab/handle.ts

0 → 100644
+104 −0
Original line number Diff line number Diff line
/**
 * Regular expression to match a fediverse handle in the format `@user@server`
 * or `user@server`.  The `user` part can contain alphanumeric characters and
 * some special characters except `@`.  The `server` part is all characters
 * after the `@` symbol in the middle.
 */
const handleRegexp =
  /^@?((?:[-A-Za-z0-9._~!$&'()*+,;=]|%[A-Fa-f0-9]{2})+)@([^@]+)$/;

/**
 * Represents a fediverse handle, which consists of a username and a host.
 * The username can be alphanumeric and may include special characters,
 * while the host is typically a domain name.
 * @since 1.8.0
 */
export interface FediverseHandle {
  /**
   * The username part of the fediverse handle.
   * It can include alphanumeric characters and some special characters.
   */
  readonly username: string;
  /**
   * The host part of the fediverse handle, typically a domain name.
   * It is the part after the `@` symbol in the handle.
   */
  readonly host: string;
}

/**
 * Parses a fediverse handle in the format `@user@server` or `user@server`.
 * The `user` part can contain alphanumeric characters and some special
 * characters except `@`.  The `server` part is all characters after the `@`
 * symbol in the middle.
 *
 * @example
 * ```typescript
 * const handle = parseFediverseHandle("@username@example.com");
 * console.log(handle?.username); // "username"
 * console.log(handle?.host);     // "example.com"
 * ```
 *
 * @param handle - The fediverse handle string to parse.
 * @returns A {@link FediverseHandle} object with `username` and `host`
 *          if the input is valid; otherwise `null`.
 * @since 1.8.0
 */
export function parseFediverseHandle(
  handle: string,
): FediverseHandle | null {
  const match = handleRegexp.exec(handle);
  if (match) {
    return {
      username: match[1],
      host: match[2],
    };
  }
  return null;
}

/**
 * Checks if a string is a valid fediverse handle in the format `@user@server`
 * or `user@server`.  The `user` part can contain alphanumeric characters and
 * some special characters except `@`.  The `server` part is all characters
 * after the `@` symbol in the middle.
 *
 * @example
 * ```typescript
 * console.log(isFediverseHandle("@username@example.com")); // true
 * console.log(isFediverseHandle("username@example.com"));  // true
 * console.log(isFediverseHandle("@username@"));            // false
 * ```
 *
 * @param handle - The string to test as a fediverse handle.
 * @returns `true` if the string matches the fediverse handle pattern;
 *          otherwise `false`.
 * @since 1.8.0
 */
export function isFediverseHandle(
  handle: string,
): handle is `${string}@${string}` {
  return handleRegexp.test(handle);
}

/**
 * Converts a fediverse handle in the format `@user@server` or `user@server`
 * to an `acct:` URI, which is a URL-like identifier for ActivityPub actors.
 *
 * @example
 * ```typescript
 * const identifier = toAcctUrl("@username@example.com");
 * console.log(identifier?.href); // "acct:username@example.com"
 * ```
 *
 * @param handle - The fediverse handle string to convert.
 * @returns A `URL` object representing the `acct:` URI if conversion succeeds;
 *          otherwise `null`.
 * @since 1.8.0
 */
export function toAcctUrl(handle: string): URL | null {
  const parsed = parseFediverseHandle(handle);
  if (!parsed) return null;
  const identifier = new URL(`acct:${parsed.username}@${parsed.host}`);
  return identifier;
}
Loading