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

Added rule and logic to detect if accounts are abusing faceting by pushing multiple facets in same byte space

Changed files
+75
src
rules
embeds
+6
src/main.ts
··· 19 19 import { checkPosts } from "./checkPosts.js"; 20 20 import { checkHandle } from "./checkHandles.js"; 21 21 import { checkDescription, checkDisplayName } from "./checkProfiles.js"; 22 + import { checkFacetSpam } from "./rules/embeds/facets.js"; 22 23 23 24 let cursor = 0; 24 25 let cursorUpdateInterval: NodeJS.Timeout; ··· 115 116 116 117 // Check if the record has facets 117 118 if (hasFacets) { 119 + // Check for facet spam (hidden mentions with duplicate byte positions) 120 + tasks.push( 121 + checkFacetSpam(event.did, event.time_us, atURI, event.commit.record.facets!), 122 + ); 123 + 118 124 const hasLinkType = event.commit.record.facets!.some((facet) => 119 125 facet.features.some( 120 126 (feature) => feature.$type === "app.bsky.richtext.facet#link",
+59
src/rules/embeds/facets.ts
··· 1 + import { createAccountLabel } from "../../moderation.js"; 2 + import { logger } from "../../logger.js"; 3 + import { Facet } from "../../types.js"; 4 + 5 + // Threshold for duplicate facet positions before flagging as spam 6 + export const FACET_SPAM_THRESHOLD = 1; 7 + 8 + // Label configuration 9 + export const FACET_SPAM_LABEL = "suspect-inauthentic"; 10 + export const FACET_SPAM_COMMENT = 11 + "Abusive facet usage detected (hidden mentions)"; 12 + 13 + /** 14 + * Checks if a post contains facet spam by detecting multiple facets 15 + * with identical byte positions (indicating hidden/abusive mentions) 16 + */ 17 + export const checkFacetSpam = async ( 18 + did: string, 19 + time: number, 20 + atURI: string, 21 + facets: Facet[], 22 + ): Promise<void> => { 23 + if (!facets || facets.length === 0) { 24 + return; 25 + } 26 + 27 + // Group facets by their byte position (byteStart:byteEnd) 28 + const positionMap = new Map<string, number>(); 29 + 30 + for (const facet of facets) { 31 + const key = `${facet.index.byteStart}:${facet.index.byteEnd}`; 32 + positionMap.set(key, (positionMap.get(key) || 0) + 1); 33 + } 34 + 35 + // Check if any position has more than the threshold 36 + for (const [position, count] of positionMap.entries()) { 37 + if (count > FACET_SPAM_THRESHOLD) { 38 + logger.info( 39 + { 40 + process: "FACET_SPAM", 41 + did, 42 + atURI, 43 + position, 44 + count, 45 + }, 46 + "Facet spam detected", 47 + ); 48 + 49 + await createAccountLabel( 50 + did, 51 + FACET_SPAM_LABEL, 52 + `${time}: ${FACET_SPAM_COMMENT} - ${count} facets at position ${position} in ${atURI}`, 53 + ); 54 + 55 + // Only label once per post even if multiple positions are suspicious 56 + return; 57 + } 58 + } 59 + };
+10
src/types.ts
··· 48 48 label: string; 49 49 rkey: string; 50 50 } 51 + 52 + export interface FacetIndex { 53 + byteStart: number; 54 + byteEnd: number; 55 + } 56 + 57 + export interface Facet { 58 + index: FacetIndex; 59 + features: Array<{ $type: string; [key: string]: any }>; 60 + }