···2525 }
26262727 // Group mention facets by their byte position (byteStart:byteEnd)
2828- // Only check mentions as duplicate tags/links are often bot bugs, not malicious
2929- const positionMap = new Map<string, number>();
2828+ // Track unique DIDs per position - only flag if DIFFERENT DIDs at same position
2929+ // Same DID duplicated = bug, different DIDs = spam
3030+ const positionMap = new Map<string, Set<string>>();
30313132 for (const facet of facets) {
3232- // Only count mentions for spam detection
3333- const hasMention = facet.features.some(
3333+ // Only check mentions for spam detection
3434+ const mentionFeature = facet.features.find(
3435 (feature) => feature.$type === "app.bsky.richtext.facet#mention"
3536 );
36373737- if (hasMention) {
3838+ if (mentionFeature && "did" in mentionFeature) {
3839 const key = `${facet.index.byteStart}:${facet.index.byteEnd}`;
3939- positionMap.set(key, (positionMap.get(key) || 0) + 1);
4040+ if (!positionMap.has(key)) {
4141+ positionMap.set(key, new Set());
4242+ }
4343+ positionMap.get(key)!.add(mentionFeature.did as string);
4044 }
4145 }
42464343- // Check if any position has more than the threshold
4444- for (const [position, count] of positionMap.entries()) {
4545- if (count > FACET_SPAM_THRESHOLD) {
4747+ // Check if any position has more than the threshold unique DIDs
4848+ for (const [position, dids] of positionMap.entries()) {
4949+ const uniqueCount = dids.size;
5050+ if (uniqueCount > FACET_SPAM_THRESHOLD) {
4651 logger.info(
4752 {
4853 process: "FACET_SPAM",
4954 did,
5055 atURI,
5156 position,
5252- count,
5757+ count: uniqueCount,
5358 },
5459 "Facet spam detected",
5560 );
···5762 await createAccountLabel(
5863 did,
5964 FACET_SPAM_LABEL,
6060- `${time}: ${FACET_SPAM_COMMENT} - ${count} facets at position ${position} in ${atURI}`,
6565+ `${time}: ${FACET_SPAM_COMMENT} - ${uniqueCount} unique mentions at position ${position} in ${atURI}`,
6166 );
62676368 // Only label once per post even if multiple positions are suspicious
+24-1
src/rules/facets/tests/facets.test.ts
···130130 expect(createAccountLabel).not.toHaveBeenCalled();
131131 expect(logger.info).not.toHaveBeenCalled();
132132 });
133133+134134+ it("should not label when same DID mentioned multiple times at same position (software bug)", async () => {
135135+ const facets: Facet[] = [
136136+ {
137137+ index: { byteStart: 0, byteEnd: 1 },
138138+ features: [{ $type: "app.bsky.richtext.facet#mention", did: "did:plc:user1" }],
139139+ },
140140+ {
141141+ index: { byteStart: 0, byteEnd: 1 },
142142+ features: [{ $type: "app.bsky.richtext.facet#mention", did: "did:plc:user1" }],
143143+ },
144144+ {
145145+ index: { byteStart: 0, byteEnd: 1 },
146146+ features: [{ $type: "app.bsky.richtext.facet#mention", did: "did:plc:user1" }],
147147+ },
148148+ ];
149149+150150+ await checkFacetSpam(TEST_DID, TEST_TIME, TEST_URI, facets);
151151+152152+ // Should not trigger - only 1 unique DID
153153+ expect(createAccountLabel).not.toHaveBeenCalled();
154154+ expect(logger.info).not.toHaveBeenCalled();
155155+ });
133156 });
134157135158 describe("when spam is detected", () => {
···161184 expect(createAccountLabel).toHaveBeenCalledWith(
162185 TEST_DID,
163186 FACET_SPAM_LABEL,
164164- `${TEST_TIME}: ${FACET_SPAM_COMMENT} - 2 facets at position 0:1 in ${TEST_URI}`
187187+ `${TEST_TIME}: ${FACET_SPAM_COMMENT} - 2 unique mentions at position 0:1 in ${TEST_URI}`
165188 );
166189 });
167190