A tool for parsing traffic on the jetstream and applying a moderation workstream based on regexp based rules

1. src/types.ts:63-64 - added optional monitoredPostURIs field, made monitoredDIDs optional (at least one must be provided) 2. src/rules/account/age.ts:13 - added optional replyToPostURI to ReplyContext interface 3. src/rules/account/age.ts:113-123 - updated logic to check both monitoredDIDs and monitoredPostURIs (matches if either condition is true) 4. src/rules/account/ageConstants.ts:27-38 - added example showing how to monitor specific post URIs 5. added 4 tests covering: - monitoring post URIs - ignoring non-matching post URIs - matching either DIDs OR post URIs - backward compatibility (works without replyToPostURI)

Skywatch 89fd7aae c4bf03e9

Changed files
+164 -4
src
+10 -2
src/rules/account/age.ts
··· 10 10 replyingDid: string; 11 11 atURI: string; 12 12 time: number; 13 + replyToPostURI?: string; // The URI of the post being replied to (optional) 13 14 } 14 15 15 16 /** ··· 109 110 110 111 // Check each configuration 111 112 for (const check of ACCOUNT_AGE_CHECKS) { 112 - // Check if this reply is to a monitored DID 113 - if (!check.monitoredDIDs.includes(context.replyToDid)) { 113 + // Check if this reply matches monitored DIDs or post URIs 114 + const matchesDID = 115 + check.monitoredDIDs && check.monitoredDIDs.includes(context.replyToDid); 116 + const matchesPostURI = 117 + check.monitoredPostURIs && 118 + context.replyToPostURI && 119 + check.monitoredPostURIs.includes(context.replyToPostURI); 120 + 121 + if (!matchesDID && !matchesPostURI) { 114 122 continue; 115 123 } 116 124
+14 -1
src/rules/account/ageConstants.ts
··· 11 11 * - Flag sock puppet accounts created to participate in coordinated harassment 12 12 */ 13 13 export const ACCOUNT_AGE_CHECKS: AccountAgeCheck[] = [ 14 - // Example configuration (disabled by default) 14 + // Example: Monitor replies to specific accounts 15 15 // { 16 16 // monitoredDIDs: [ 17 17 // "did:plc:example123", // High-profile account 1 ··· 22 22 // label: "new-account-reply", 23 23 // comment: "New account replying to monitored user during campaign", 24 24 // expires: "2025-02-15", // Optional: automatically stop this check after this date 25 + // }, 26 + // 27 + // Example: Monitor replies to specific posts 28 + // { 29 + // monitoredPostURIs: [ 30 + // "at://did:plc:example123/app.bsky.feed.post/abc123", 31 + // "at://did:plc:example456/app.bsky.feed.post/def456", 32 + // ], 33 + // anchorDate: "2025-01-15", 34 + // maxAgeDays: 7, 35 + // label: "brigading-suspect", 36 + // comment: "New account replying to specific targeted post", 37 + // expires: "2025-02-15", 25 38 // }, 26 39 ];
+138
src/rules/account/tests/age.test.ts
··· 571 571 expect.stringContaining("Account created within monitored range"), 572 572 ); 573 573 }); 574 + 575 + it("should label account when reply is to a monitored post URI", async () => { 576 + ACCOUNT_AGE_CHECKS.push({ 577 + monitoredPostURIs: [ 578 + "at://did:plc:monitored/app.bsky.feed.post/specificpost", 579 + ], 580 + anchorDate: "2025-10-15", 581 + maxAgeDays: 7, 582 + label: "brigading-suspect", 583 + comment: "New account replying to targeted post", 584 + }); 585 + 586 + // Mock account created within window 587 + const mockDidDoc = [{ createdAt: "2025-10-18T12:00:00.000Z" }]; 588 + (global.fetch as any).mockResolvedValueOnce({ 589 + ok: true, 590 + json: async () => mockDidDoc, 591 + }); 592 + 593 + // Mock that label does NOT exist 594 + (checkAccountLabels as any).mockResolvedValueOnce(false); 595 + 596 + await checkAccountAge({ 597 + replyToDid: "did:plc:someone", 598 + replyingDid: "did:plc:newaccount", 599 + atURI: TEST_REPLY_URI, 600 + time: TEST_TIME, 601 + replyToPostURI: "at://did:plc:monitored/app.bsky.feed.post/specificpost", 602 + }); 603 + 604 + expect(createAccountLabel).toHaveBeenCalledWith( 605 + "did:plc:newaccount", 606 + "brigading-suspect", 607 + expect.stringContaining("Account created within monitored range"), 608 + ); 609 + }); 610 + 611 + it("should not label when reply is to different post URI", async () => { 612 + ACCOUNT_AGE_CHECKS.push({ 613 + monitoredPostURIs: [ 614 + "at://did:plc:monitored/app.bsky.feed.post/specificpost", 615 + ], 616 + anchorDate: "2025-10-15", 617 + maxAgeDays: 7, 618 + label: "brigading-suspect", 619 + comment: "New account replying to targeted post", 620 + }); 621 + 622 + // Mock account created within window 623 + const mockDidDoc = [{ createdAt: "2025-10-18T12:00:00.000Z" }]; 624 + (global.fetch as any).mockResolvedValueOnce({ 625 + ok: true, 626 + json: async () => mockDidDoc, 627 + }); 628 + 629 + await checkAccountAge({ 630 + replyToDid: "did:plc:someone", 631 + replyingDid: "did:plc:newaccount", 632 + atURI: TEST_REPLY_URI, 633 + time: TEST_TIME, 634 + replyToPostURI: "at://did:plc:other/app.bsky.feed.post/differentpost", 635 + }); 636 + 637 + expect(createAccountLabel).not.toHaveBeenCalled(); 638 + }); 639 + 640 + it("should match either monitored DID or post URI", async () => { 641 + ACCOUNT_AGE_CHECKS.push({ 642 + monitoredDIDs: ["did:plc:monitored1"], 643 + monitoredPostURIs: [ 644 + "at://did:plc:monitored2/app.bsky.feed.post/specificpost", 645 + ], 646 + anchorDate: "2025-10-15", 647 + maxAgeDays: 7, 648 + label: "multi-monitor", 649 + comment: "New account replying to monitored target", 650 + }); 651 + 652 + // Mock account created within window 653 + const mockDidDoc = [{ createdAt: "2025-10-18T12:00:00.000Z" }]; 654 + (global.fetch as any).mockResolvedValueOnce({ 655 + ok: true, 656 + json: async () => mockDidDoc, 657 + }); 658 + 659 + // Mock that label does NOT exist 660 + (checkAccountLabels as any).mockResolvedValueOnce(false); 661 + 662 + // Test matching by post URI even though DID doesn't match 663 + await checkAccountAge({ 664 + replyToDid: "did:plc:someone-else", 665 + replyingDid: "did:plc:newaccount", 666 + atURI: TEST_REPLY_URI, 667 + time: TEST_TIME, 668 + replyToPostURI: 669 + "at://did:plc:monitored2/app.bsky.feed.post/specificpost", 670 + }); 671 + 672 + expect(createAccountLabel).toHaveBeenCalledWith( 673 + "did:plc:newaccount", 674 + "multi-monitor", 675 + expect.stringContaining("Account created within monitored range"), 676 + ); 677 + }); 678 + 679 + it("should work without replyToPostURI (backward compatibility)", async () => { 680 + ACCOUNT_AGE_CHECKS.push({ 681 + monitoredDIDs: ["did:plc:monitored"], 682 + anchorDate: "2025-10-15", 683 + maxAgeDays: 7, 684 + label: "window-reply", 685 + comment: "New account reply", 686 + }); 687 + 688 + // Mock account created within window 689 + const mockDidDoc = [{ createdAt: "2025-10-18T12:00:00.000Z" }]; 690 + (global.fetch as any).mockResolvedValueOnce({ 691 + ok: true, 692 + json: async () => mockDidDoc, 693 + }); 694 + 695 + // Mock that label does NOT exist 696 + (checkAccountLabels as any).mockResolvedValueOnce(false); 697 + 698 + await checkAccountAge({ 699 + replyToDid: "did:plc:monitored", 700 + replyingDid: "did:plc:newaccount", 701 + atURI: TEST_REPLY_URI, 702 + time: TEST_TIME, 703 + // No replyToPostURI provided 704 + }); 705 + 706 + expect(createAccountLabel).toHaveBeenCalledWith( 707 + "did:plc:newaccount", 708 + "window-reply", 709 + expect.stringContaining("Account created within monitored range"), 710 + ); 711 + }); 574 712 }); 575 713 });
+2 -1
src/types.ts
··· 60 60 } 61 61 62 62 export interface AccountAgeCheck { 63 - monitoredDIDs: string[]; // DIDs to monitor for replies 63 + monitoredDIDs?: string[]; // DIDs to monitor for replies (optional if monitoredPostURIs is provided) 64 + monitoredPostURIs?: string[]; // Specific post URIs to monitor for replies (optional if monitoredDIDs is provided) 64 65 anchorDate: string; // ISO 8601 date string (e.g., "2025-01-15") 65 66 maxAgeDays: number; // Maximum account age in days 66 67 label: string; // Label to apply if account is too new