Skip to content

Commit dae4d16

Browse files
committed
Add -t/--traverse option to fedify lookup
Close #195
1 parent 8516e73 commit dae4d16

File tree

5 files changed

+1388
-485
lines changed

5 files changed

+1388
-485
lines changed

CHANGES.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,17 @@ Version 1.4.0
88

99
To be released.
1010

11+
- The `suppressError` option of Activity Vocabulary APIs,
12+
`traverseCollection()` function, and `Context.traverseCollection()` method
13+
now suppresses errors occurred JSON-LD processing.
14+
15+
- Added `-t`/`--traverse` option to the `fedify lookup` subcommand. [[#195]]
16+
17+
- Added `-S`/`--suppress-errors` option to the `fedify lookup` subcommand.
18+
[[#195]]
19+
20+
[#195]: https://github.com/dahlia/fedify/issues/195
21+
1122

1223
Version 1.3.1
1324
-------------

cli/lookup.ts

Lines changed: 110 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,44 @@ import { colors } from "@cliffy/ansi";
22
import { Command } from "@cliffy/command";
33
import {
44
Application,
5+
Collection,
56
CryptographicKey,
67
type DocumentLoader,
78
generateCryptoKeyPair,
89
getAuthenticatedDocumentLoader,
10+
type Link,
911
lookupObject,
1012
type Object,
1113
type ResourceDescriptor,
1214
respondWithObject,
15+
traverseCollection,
1316
} from "@fedify/fedify";
17+
import { getLogger } from "@logtape/logtape";
1418
import ora from "ora";
1519
import { getContextLoader, getDocumentLoader } from "./docloader.ts";
1620
import { spawnTemporaryServer, type TemporaryServer } from "./tempserver.ts";
1721
import { printJson } from "./utils.ts";
1822

23+
const logger = getLogger(["fedify", "cli", "lookup"]);
24+
1925
export const command = new Command()
2026
.arguments("<...urls:string>")
2127
.description(
2228
"Lookup an Activity Streams object by URL or the actor handle. " +
2329
"The argument can be either a URL or an actor handle " +
24-
"(e.g., @username@domain).",
30+
"(e.g., @username@domain), and it can be multiple.",
2531
)
2632
.option("-a, --authorized-fetch", "Sign the request with an one-time key.")
33+
.option(
34+
"-t, --traverse",
35+
"Traverse the given collection to fetch all items. If it is turned on, " +
36+
"the argument cannot be multiple.",
37+
)
38+
.option(
39+
"-S, --suppress-errors",
40+
"Suppress partial errors while traversing the collection.",
41+
{ depends: ["traverse"] },
42+
)
2743
.option("-r, --raw", "Print the fetched JSON-LD document as is.", {
2844
conflicts: ["compact", "expand"],
2945
})
@@ -36,12 +52,24 @@ export const command = new Command()
3652
.option("-u, --user-agent <string>", "The custom User-Agent header value.")
3753
.option(
3854
"-s, --separator <string>",
39-
"Specify the separator between adjacent output objects.",
55+
"Specify the separator between adjacent output objects or " +
56+
"collection items.",
4057
{ default: "----" },
4158
)
4259
.action(async (options, ...urls: string[]) => {
60+
if (urls.length < 1) {
61+
console.error("At least one URL or actor handle must be provided.");
62+
Deno.exit(1);
63+
} else if (options.traverse && urls.length > 1) {
64+
console.error(
65+
"The -t/--traverse option cannot be used with multiple arguments.",
66+
);
67+
Deno.exit(1);
68+
}
4369
const spinner = ora({
44-
text: "Looking up the object...",
70+
text: `Looking up the ${
71+
options.traverse ? "collection" : urls.length > 1 ? "objects" : "object"
72+
}...`,
4573
discardStdin: false,
4674
}).start();
4775
let server: TemporaryServer | undefined = undefined;
@@ -95,9 +123,84 @@ export const command = new Command()
95123
privateKey: key.privateKey,
96124
});
97125
}
98-
spinner.text = urls.length > 1
99-
? "Looking up objects..."
100-
: "Looking up an object...";
126+
spinner.text = `Looking up the ${
127+
options.traverse ? "collection" : urls.length > 1 ? "objects" : "object"
128+
}...`;
129+
130+
async function printObject(object: Object | Link): Promise<void> {
131+
if (options.raw) {
132+
printJson(await object.toJsonLd({ contextLoader }));
133+
} else if (options.compact) {
134+
printJson(
135+
await object.toJsonLd({ format: "compact", contextLoader }),
136+
);
137+
} else if (options.expand) {
138+
printJson(
139+
await object.toJsonLd({ format: "expand", contextLoader }),
140+
);
141+
} else {
142+
console.log(object);
143+
}
144+
}
145+
146+
if (options.traverse) {
147+
const url = urls[0];
148+
const collection = await lookupObject(url, {
149+
documentLoader: authLoader ?? documentLoader,
150+
contextLoader,
151+
userAgent: options.userAgent,
152+
});
153+
if (collection == null) {
154+
spinner.fail(`Failed to fetch object: ${colors.red(url)}.`);
155+
if (authLoader == null) {
156+
console.error(
157+
"It may be a private object. Try with -a/--authorized-fetch.",
158+
);
159+
}
160+
await server?.close();
161+
Deno.exit(1);
162+
}
163+
if (!(collection instanceof Collection)) {
164+
spinner.fail(
165+
`Not a collection: ${colors.red(url)}. ` +
166+
"The -t/--traverse option requires a collection.",
167+
);
168+
await server?.close();
169+
Deno.exit(1);
170+
}
171+
spinner.succeed(`Fetched collection: ${colors.green(url)}.`);
172+
try {
173+
let i = 0;
174+
for await (
175+
const item of traverseCollection(collection, {
176+
documentLoader: authLoader ?? documentLoader,
177+
contextLoader,
178+
suppressError: options.suppressErrors,
179+
})
180+
) {
181+
if (i > 0) console.log(options.separator);
182+
printObject(item);
183+
i++;
184+
}
185+
} catch (error) {
186+
logger.error("Failed to complete the traversal: {error}", { error });
187+
spinner.fail("Failed to complete the traversal.");
188+
if (authLoader == null) {
189+
console.error(
190+
"It may be a private object. Try with -a/--authorized-fetch.",
191+
);
192+
} else {
193+
console.error(
194+
"Use the -S/--suppress-errors option to suppress partial errors.",
195+
);
196+
}
197+
await server?.close();
198+
Deno.exit(1);
199+
}
200+
spinner.succeed("Successfully fetched all items in the collection.");
201+
await server?.close();
202+
Deno.exit(0);
203+
}
101204

102205
const promises: Promise<Object | null>[] = [];
103206
for (const url of urls) {
@@ -131,19 +234,7 @@ export const command = new Command()
131234
success = false;
132235
} else {
133236
spinner.succeed(`Fetched object: ${colors.green(url)}.`);
134-
if (options.raw) {
135-
printJson(await object.toJsonLd({ contextLoader }));
136-
} else if (options.compact) {
137-
printJson(
138-
await object.toJsonLd({ format: "compact", contextLoader }),
139-
);
140-
} else if (options.expand) {
141-
printJson(
142-
await object.toJsonLd({ format: "expand", contextLoader }),
143-
);
144-
} else {
145-
console.log(object);
146-
}
237+
printObject(object);
147238
if (i < urls.length - 1) {
148239
console.log(options.separator);
149240
}

docs/cli.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,47 @@ As you can see, the outputs are separated by `----` by default. You can change
305305
the separator by using the [`-s`/`--separator`](#s-separator-output-separator)
306306
option.
307307

308+
> [!NOTE]
309+
> The `fedify lookup` command cannot take multiple argument if
310+
> [`-t`/`--traverse`](#t-traverse-traverse-the-collection) option is turned
311+
> on.
312+
313+
### `-t`/`--traverse`: Traverse the collection
314+
315+
*This option is available since Fedify 0.14.0.*
316+
317+
The `-t`/`--traverse` option is used to traverse the collection when looking up
318+
a collection object. For example, the below command looks up a collection
319+
object:
320+
321+
~~~~ sh
322+
fedify lookup --traverse https://fosstodon.org/users/hongminhee/outbox
323+
~~~~
324+
325+
The difference between with and without the `-t`/`--traverse` option is that
326+
the former will output the objects in the collection, while the latter will
327+
output the collection object itself.
328+
329+
This option only works with a single argument, and it has to be a collection.
330+
331+
### `-S`/`--suppress-errors`: Suppress partial errors during traversal
332+
333+
*This option is available since Fedify 0.14.0.*
334+
335+
The `-S`/`--suppress-errors` option is used to suppress partial errors during
336+
traversal. For example, the below command looks up a collection object with
337+
the `-t`/`--traverse` option:
338+
339+
~~~~ sh
340+
fedify lookup --traverse --suppress-errors https://fosstodon.org/users/hongminhee/outbox
341+
~~~~
342+
343+
The difference between with and without the `-S`/`--suppress-errors` option is
344+
that the former will suppress the partial errors during traversal, while the
345+
latter will stop the traversal when an error occurs.
346+
347+
This option depends on the `-t`/`--traverse` option.
348+
308349
### `-c`/`--compact`: Compact JSON-LD
309350

310351
> [!NOTE]
@@ -692,6 +733,10 @@ fedify lookup -s ==== @[email protected] @[email protected]
692733

693734
It does not affect the output when looking up a single object.
694735

736+
> [!TIP]
737+
> The separator is also used when looking up a collection object with the
738+
> [`-t`/`--traverse`](#t-traverse-traverse-the-collection) option.
739+
695740

696741
`fedify inbox`: Ephemeral inbox server
697742
--------------------------------------

0 commit comments

Comments
 (0)