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

refactor: Move rule files into subdirectories

Move rule-related files into dedicated subdirectories to improve
organization.

Skywatch 98bae0e9 c4af1998

+153 -49
+1
.gitignore
··· 5 5 labels.db* 6 6 .DS_Store 7 7 src/constants.ts 8 + constants.ts
+3 -3
src/checkHandles.ts src/rules/handles/checkHandles.ts
··· 1 1 import { HANDLE_CHECKS } from "./constants.js"; 2 - import { logger } from "./logger.js"; 2 + import { logger } from "../../logger.js"; 3 3 import { 4 4 createAccountReport, 5 5 createAccountComment, 6 6 createAccountLabel, 7 - } from "./moderation.js"; 8 - import { GLOBAL_ALLOW } from "./constants.js"; 7 + } from "../../moderation.js"; 8 + import { GLOBAL_ALLOW } from "../../constants.js"; 9 9 10 10 export const checkHandle = async ( 11 11 did: string,
+7 -7
src/checkPosts.ts src/rules/posts/checkPosts.ts
··· 1 - import { LINK_SHORTENER, POST_CHECKS } from "./constants.js"; 2 - import { Post } from "./types.js"; 3 - import { logger } from "./logger.js"; 4 - import { countStarterPacks } from "./count.js"; 1 + import { POST_CHECKS } from "./constants.js"; 2 + import { Post } from "../../types.js"; 3 + import { logger } from "../../logger.js"; 4 + import { countStarterPacks } from "../account/countStarterPacks.js"; 5 5 import { 6 6 createPostLabel, 7 7 createAccountReport, 8 8 createAccountComment, 9 9 createPostReport, 10 - } from "./moderation.js"; 11 - import { getFinalUrl, getLanguage } from "./utils.js"; 12 - import { GLOBAL_ALLOW } from "./constants.js"; 10 + } from "../../moderation.js"; 11 + import { getFinalUrl, getLanguage } from "../../utils.js"; 12 + import { LINK_SHORTENER, GLOBAL_ALLOW } from "../../constants.js"; 13 13 14 14 export const checkPosts = async (post: Post[]) => { 15 15 if (GLOBAL_ALLOW.includes(post[0].did)) {
+5 -5
src/checkProfiles.ts src/rules/profiles/checkProfiles.ts
··· 1 - import { PROFILE_CHECKS } from "./constants.js"; 2 - import { logger } from "./logger.js"; 1 + import { PROFILE_CHECKS } from "../../rules/profiles/constants.js"; 2 + import { logger } from "../../logger.js"; 3 3 import { 4 4 createAccountReport, 5 5 createAccountLabel, 6 6 createAccountComment, 7 - } from "./moderation.js"; 8 - import { getLanguage } from "./utils.js"; 9 - import { GLOBAL_ALLOW } from "./constants.js"; 7 + } from "../../moderation.js"; 8 + import { getLanguage } from "../../utils.js"; 9 + import { GLOBAL_ALLOW } from "../../constants.js"; 10 10 11 11 export const checkDescription = async ( 12 12 did: string,
-30
src/count.ts
··· 1 - import { isLoggedIn, agent } from "./agent.js"; 2 - import { logger } from "./logger.js"; 3 - import { limit } from "./limits.js"; 4 - import { createAccountLabel } from "./moderation.js"; 5 - 6 - export const countStarterPacks = async (did: string, time: number) => { 7 - await isLoggedIn; 8 - 9 - if (did in ["did:plc:gpunjjgvlyb4racypz3yfiq4"]) { 10 - logger.debug({ process: "COUNTSTARTERPACKS", did, time }, "Account is whitelisted"); 11 - return; 12 - } 13 - 14 - await limit(async () => { 15 - try { 16 - const profile = await agent.app.bsky.actor.getProfile({ actor: did }); 17 - const starterPacks = profile.data.associated?.starterPacks; 18 - 19 - if (starterPacks && starterPacks.valueOf() > 20) { 20 - createAccountLabel( 21 - did, 22 - "follow-farming", 23 - `[COUNTSTARTERPACKS]: ${time}: Account ${did} has ${starterPacks} starter packs.`, 24 - ); 25 - } 26 - } catch (error) { 27 - logger.error({ process: "COUNTSTARTERPACKS", error }, "Error checking associated accounts"); 28 - } 29 - }); 30 - };
+9 -4
src/main.ts
··· 15 15 import { logger } from "./logger.js"; 16 16 import { startMetricsServer } from "./metrics.js"; 17 17 import { Post, LinkFeature, Handle } from "./types.js"; 18 - import { checkPosts } from "./checkPosts.js"; 19 - import { checkHandle } from "./checkHandles.js"; 20 - import { checkDescription, checkDisplayName } from "./checkProfiles.js"; 18 + import { checkPosts } from "./rules/posts/checkPosts.js"; 19 + import { checkHandle } from "./rules/handles/checkHandles.js"; 20 + import { 21 + checkDescription, 22 + checkDisplayName, 23 + } from "./rules/profiles/checkProfiles.js"; 21 24 import { checkFacetSpam } from "./rules/facets/facets.js"; 22 25 import { checkAccountAge } from "./rules/account/age.js"; 23 26 ··· 139 142 embed.$type === "app.bsky.embed.recordWithMedia") 140 143 ) { 141 144 const record = 142 - embed.$type === "app.bsky.embed.record" ? embed.record : embed.record.record; 145 + embed.$type === "app.bsky.embed.record" 146 + ? embed.record 147 + : embed.record.record; 143 148 if (record && record.uri) { 144 149 const quotedPostURI = record.uri; 145 150 const quotedDid = quotedPostURI.split("/")[2]; // Extract DID from at://did/...
+36
src/rules/account/countStarterPacks.ts
··· 1 + import { isLoggedIn, agent } from "../../agent.js"; 2 + import { logger } from "../../logger.js"; 3 + import { limit } from "../../limits.js"; 4 + import { createAccountLabel } from "../../moderation.js"; 5 + 6 + export const countStarterPacks = async (did: string, time: number) => { 7 + await isLoggedIn; 8 + 9 + if (did in ["did:plc:gpunjjgvlyb4racypz3yfiq4"]) { 10 + logger.debug( 11 + { process: "COUNTSTARTERPACKS", did, time }, 12 + "Account is whitelisted", 13 + ); 14 + return; 15 + } 16 + 17 + await limit(async () => { 18 + try { 19 + const profile = await agent.app.bsky.actor.getProfile({ actor: did }); 20 + const starterPacks = profile.data.associated?.starterPacks; 21 + 22 + if (starterPacks && starterPacks.valueOf() > 20) { 23 + createAccountLabel( 24 + did, 25 + "follow-farming", 26 + `[COUNTSTARTERPACKS]: ${time}: Account ${did} has ${starterPacks} starter packs.`, 27 + ); 28 + } 29 + } catch (error) { 30 + logger.error( 31 + { process: "COUNTSTARTERPACKS", error }, 32 + "Error checking associated accounts", 33 + ); 34 + } 35 + }); 36 + };
+92
src/rules/handles/constants.example.ts
··· 1 + import { Checks } from "../../types.js"; 2 + 3 + /** 4 + * Example handle check configurations 5 + * 6 + * This file demonstrates how to configure handle-based moderation rules. 7 + * Copy this file to constants.ts and customize for your labeler's needs. 8 + * 9 + * Each check can match against handles, display names, and/or descriptions 10 + * based on the flags you set (description: true, displayName: true). 11 + */ 12 + 13 + export const HANDLE_CHECKS: Checks[] = [ 14 + // Example 1: Simple pattern matching with whitelist 15 + { 16 + label: "spam-indicator", 17 + comment: "Handle matches common spam patterns", 18 + reportAcct: false, 19 + commentAcct: false, 20 + toLabel: true, 21 + check: new RegExp( 22 + "follow.*?back|gain.*?followers|crypto.*?giveaway|free.*?money", 23 + "i", 24 + ), 25 + whitelist: new RegExp("legitimate.*?business", "i"), 26 + }, 27 + 28 + // Example 2: Check specific domain patterns 29 + { 30 + label: "suspicious-domain", 31 + comment: "Handle uses suspicious domain pattern", 32 + reportAcct: false, 33 + commentAcct: false, 34 + toLabel: true, 35 + check: new RegExp("(?:suspicious-site\\.example)", "i"), 36 + }, 37 + 38 + // Example 3: Check with display name and description matching 39 + { 40 + label: "potential-impersonator", 41 + comment: "Account may be impersonating verified entities", 42 + description: true, 43 + displayName: true, 44 + reportAcct: false, 45 + commentAcct: false, 46 + toLabel: true, 47 + check: new RegExp( 48 + "official.*?support|customer.*?service.*?rep|verified.*?account", 49 + "i", 50 + ), 51 + // Exclude accounts that are actually legitimate 52 + ignoredDIDs: [ 53 + "did:plc:example123", // Real customer support account 54 + "did:plc:example456", // Verified business account 55 + ], 56 + }, 57 + 58 + // Example 4: Pattern with specific character variations 59 + { 60 + label: "suspicious-pattern", 61 + comment: "Handle contains suspicious character patterns", 62 + reportAcct: false, 63 + commentAcct: false, 64 + toLabel: true, 65 + check: new RegExp( 66 + "[a-z]{2,}[0-9]{6,}|random.*?numbers.*?[0-9]{4,}", 67 + "i", 68 + ), 69 + whitelist: new RegExp("year[0-9]{4}", "i"), 70 + ignoredDIDs: [ 71 + "did:plc:example789", // Legitimate account with number pattern 72 + ], 73 + }, 74 + 75 + // Example 5: Brand protection 76 + { 77 + label: "brand-impersonation", 78 + comment: "Potential brand impersonation detected", 79 + reportAcct: false, 80 + commentAcct: false, 81 + toLabel: true, 82 + check: new RegExp("example-?brand|cool-?company|awesome-?corp", "i"), 83 + whitelist: new RegExp( 84 + "anti-example-brand|not-cool-company|parody.*awesome-corp", 85 + "i", 86 + ), 87 + ignoredDIDs: [ 88 + "did:plc:exampleabc", // Official brand account 89 + "did:plc:exampledef", // Authorized partner 90 + ], 91 + }, 92 + ];
src/rules/posts/constants.example.ts

This is a binary file and will not be displayed.

src/rules/profiles/constants.example.ts

This is a binary file and will not be displayed.