···2424 return;
2525 }
26262727- // Group facets by their byte position (byteStart:byteEnd)
2727+ // Group mention facets by their byte position (byteStart:byteEnd)
2828+ // Only check mentions as duplicate tags/links are often bot bugs, not malicious
2829 const positionMap = new Map<string, number>();
29303031 for (const facet of facets) {
3131- const key = `${facet.index.byteStart}:${facet.index.byteEnd}`;
3232- positionMap.set(key, (positionMap.get(key) || 0) + 1);
3232+ // Only count mentions for spam detection
3333+ const hasMention = facet.features.some(
3434+ (feature) => feature.$type === "app.bsky.richtext.facet#mention"
3535+ );
3636+3737+ if (hasMention) {
3838+ const key = `${facet.index.byteStart}:${facet.index.byteEnd}`;
3939+ positionMap.set(key, (positionMap.get(key) || 0) + 1);
4040+ }
3341 }
34423543 // Check if any position has more than the threshold
+44-2
src/rules/facets/tests/facets.test.ts
···92929393 expect(createAccountLabel).not.toHaveBeenCalled();
9494 });
9595+9696+ it("should not label when duplicate tags/links at same position (bot bugs)", async () => {
9797+ const facets: Facet[] = [
9898+ {
9999+ index: { byteStart: 38, byteEnd: 43 },
100100+ features: [{ $type: "app.bsky.richtext.facet#tag", tag: "news" }],
101101+ },
102102+ {
103103+ index: { byteStart: 38, byteEnd: 43 },
104104+ features: [{ $type: "app.bsky.richtext.facet#tag", tag: "News" }],
105105+ },
106106+ ];
107107+108108+ await checkFacetSpam(TEST_DID, TEST_TIME, TEST_URI, facets);
109109+110110+ // Should not trigger - only mentions are checked
111111+ expect(createAccountLabel).not.toHaveBeenCalled();
112112+ expect(logger.info).not.toHaveBeenCalled();
113113+ });
114114+115115+ it("should not label when duplicate links at same position", async () => {
116116+ const facets: Facet[] = [
117117+ {
118118+ index: { byteStart: 0, byteEnd: 10 },
119119+ features: [{ $type: "app.bsky.richtext.facet#link", uri: "https://example.com" }],
120120+ },
121121+ {
122122+ index: { byteStart: 0, byteEnd: 10 },
123123+ features: [{ $type: "app.bsky.richtext.facet#link", uri: "https://example.org" }],
124124+ },
125125+ ];
126126+127127+ await checkFacetSpam(TEST_DID, TEST_TIME, TEST_URI, facets);
128128+129129+ // Should not trigger - only mentions are checked
130130+ expect(createAccountLabel).not.toHaveBeenCalled();
131131+ expect(logger.info).not.toHaveBeenCalled();
132132+ });
95133 });
9613497135 describe("when spam is detected", () => {
···178216 expect(createAccountLabel).toHaveBeenCalledOnce();
179217 });
180218181181- it("should handle different feature types at same position", async () => {
219219+ it("should handle mixed feature types - only mentions at same position count", async () => {
182220 const facets: Facet[] = [
183221 {
184222 index: { byteStart: 0, byteEnd: 1 },
···186224 },
187225 {
188226 index: { byteStart: 0, byteEnd: 1 },
227227+ features: [{ $type: "app.bsky.richtext.facet#mention", did: "did:plc:user2" }],
228228+ },
229229+ {
230230+ index: { byteStart: 0, byteEnd: 1 },
189231 features: [{ $type: "app.bsky.richtext.facet#link", uri: "https://example.com" }],
190232 },
191233 ];
192234193235 await checkFacetSpam(TEST_DID, TEST_TIME, TEST_URI, facets);
194236195195- // Should still detect as spam regardless of feature type
237237+ // Should detect spam (2 mentions at same position)
196238 expect(createAccountLabel).toHaveBeenCalledOnce();
197239 });
198240 });