Mass Block [bsky] Reposts [and more]
1import { test } from "node:test";
2import assert from "node:assert/strict";
3import { readFile, unlink } from "node:fs/promises";
4import { tmpdir } from "node:os";
5import { join } from "node:path";
6import { isProfileUrl, resolveMiniDoc, filterCandidates, writeOutput } from "./index.js";
7
8test("isProfileUrl: profile URLs", () => {
9 assert.equal(isProfileUrl("https://bsky.app/profile/alice.bsky.social"), true);
10 assert.equal(isProfileUrl("https://witchsky.app/profile/jim.bsky.social"), true);
11 assert.equal(isProfileUrl("https://bsky.app/profile/did:plc:abc123"), true);
12});
13
14test("isProfileUrl: post URLs are rejected", () => {
15 assert.equal(isProfileUrl("https://bsky.app/profile/alice.bsky.social/post/abc"), false);
16 assert.equal(isProfileUrl("at://did:plc:abc/app.bsky.feed.post/rkey"), false);
17});
18
19test("isProfileUrl: unrelated URLs are rejected", () => {
20 assert.equal(isProfileUrl("https://bsky.app/"), false);
21 assert.equal(isProfileUrl("https://bsky.app/search"), false);
22});
23
24test("resolveMiniDoc returns did, handle, pds for a known handle", async () => {
25 // bad-example.com is slingshot's own example account -- stable
26 const result = await resolveMiniDoc("bad-example.com");
27 assert.equal(typeof result.did, "string");
28 assert.ok(result.did.startsWith("did:"));
29 assert.equal(typeof result.handle, "string");
30 assert.equal(typeof result.pds, "string");
31 assert.ok(result.pds.startsWith("https://"));
32});
33
34test("resolveMiniDoc accepts a DID as identifier", async () => {
35 // did:plc:hdhoaan3xa3jiuq4fg4mefid is bad-example.com per slingshot docs
36 const result = await resolveMiniDoc("did:plc:hdhoaan3xa3jiuq4fg4mefid");
37 assert.equal(typeof result.did, "string");
38 assert.equal(typeof result.pds, "string");
39 assert.equal(typeof result.handle, "string");
40});
41
42const BASE_FILTER_ARGS = {
43 results: { reposts: [], likes: [], replies: [], followers: [], following: [], quotes: [], quotePosters: [] },
44 did: "did:plc:me",
45 follows: new Set(),
46 followers: new Set(),
47 existingBlocks: new Set(),
48 blockFollowing: false,
49};
50
51test("filterCandidates: blocks target by default", () => {
52 const result = filterCandidates({
53 ...BASE_FILTER_ARGS,
54 results: { ...BASE_FILTER_ARGS.results, reposts: ["did:plc:reposter"] },
55 targetDid: "did:plc:target",
56 blockTarget: true,
57 });
58 assert.ok(result.toBlock.includes("did:plc:target"));
59});
60
61test("filterCandidates: skips target when blockTarget is false", () => {
62 const result = filterCandidates({
63 ...BASE_FILTER_ARGS,
64 results: { ...BASE_FILTER_ARGS.results, reposts: ["did:plc:reposter"] },
65 targetDid: "did:plc:target",
66 blockTarget: false,
67 });
68 assert.ok(!result.toBlock.includes("did:plc:target"));
69});
70
71test("filterCandidates: excludes quote posters by default", () => {
72 const result = filterCandidates({
73 ...BASE_FILTER_ARGS,
74 results: { ...BASE_FILTER_ARGS.results, quotePosters: ["did:plc:quoter"] },
75 targetDid: null,
76 blockQuotes: false,
77 });
78 assert.ok(!result.toBlock.includes("did:plc:quoter"));
79});
80
81test("filterCandidates: includes quote posters when blockQuotes is true", () => {
82 const result = filterCandidates({
83 ...BASE_FILTER_ARGS,
84 results: { ...BASE_FILTER_ARGS.results, quotes: ["did:plc:quoter"], quotePosters: ["did:plc:quoter"] },
85 targetDid: null,
86 blockQuotes: true,
87 });
88 assert.ok(result.toBlock.includes("did:plc:quoter"));
89});
90
91test("writeOutput: writes one DID per line", async () => {
92 const path = join(tmpdir(), `blocker-test-${Date.now()}.txt`);
93 const dids = ["did:plc:aaa", "did:plc:bbb", "did:plc:ccc"];
94 await writeOutput(path, dids);
95 const content = await readFile(path, "utf8");
96 assert.equal(content, "did:plc:aaa\ndid:plc:bbb\ndid:plc:ccc\n");
97 await unlink(path);
98});