+6
src/main.ts
+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
+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
+
};