A tool for parsing traffic on the jetstream and applying a moderation workstream based on regexp based rules
1import { ACCOUNT_THRESHOLD_CONFIGS } from "../rules/accountThreshold.js";
2import {
3 createAccountComment,
4 createAccountLabel,
5 createAccountReport,
6} from "./accountModeration.js";
7import { logger } from "./logger.js";
8import {
9 accountLabelsThresholdAppliedCounter,
10 accountThresholdChecksCounter,
11 accountThresholdMetCounter,
12} from "./metrics.js";
13import {
14 getPostLabelCountInWindow,
15 trackPostLabelForAccount,
16} from "./redis.js";
17import type { AccountThresholdConfig } from "./types.js";
18
19function normalizeLabels(labels: string | string[]): string[] {
20 return Array.isArray(labels) ? labels : [labels];
21}
22
23function validateAndLoadConfigs(): AccountThresholdConfig[] {
24 if (ACCOUNT_THRESHOLD_CONFIGS.length === 0) {
25 logger.warn(
26 { process: "ACCOUNT_THRESHOLD" },
27 "No account threshold configs found",
28 );
29 return [];
30 }
31
32 for (const config of ACCOUNT_THRESHOLD_CONFIGS) {
33 const labels = normalizeLabels(config.labels);
34 if (labels.length === 0) {
35 throw new Error(
36 `Invalid account threshold config: labels cannot be empty`,
37 );
38 }
39 if (config.threshold <= 0) {
40 throw new Error(
41 `Invalid account threshold config: threshold must be positive`,
42 );
43 }
44 if (config.window <= 0) {
45 throw new Error(
46 `Invalid account threshold config: window must be positive`,
47 );
48 }
49 }
50
51 logger.info(
52 { process: "ACCOUNT_THRESHOLD", count: ACCOUNT_THRESHOLD_CONFIGS.length },
53 "Loaded account threshold configs",
54 );
55
56 return ACCOUNT_THRESHOLD_CONFIGS;
57}
58
59// Load and cache configs at module initialization
60const cachedConfigs = validateAndLoadConfigs();
61
62export function loadThresholdConfigs(): AccountThresholdConfig[] {
63 return cachedConfigs;
64}
65
66export async function checkAccountThreshold(
67 did: string,
68 uri: string,
69 postLabel: string,
70 timestamp: number,
71): Promise<void> {
72 try {
73 const configs = loadThresholdConfigs();
74
75 const matchingConfigs = configs.filter((config) => {
76 const labels = normalizeLabels(config.labels);
77 return labels.includes(postLabel);
78 });
79
80 if (matchingConfigs.length === 0) {
81 logger.debug(
82 { process: "ACCOUNT_THRESHOLD", did, postLabel },
83 "No matching threshold configs for post label",
84 );
85 return;
86 }
87
88 accountThresholdChecksCounter.inc({ post_label: postLabel });
89
90 for (const config of matchingConfigs) {
91 const labels = normalizeLabels(config.labels);
92
93 await trackPostLabelForAccount(
94 did,
95 postLabel,
96 timestamp,
97 config.window,
98 config.windowUnit,
99 );
100
101 const count = await getPostLabelCountInWindow(
102 did,
103 labels,
104 config.window,
105 config.windowUnit,
106 timestamp,
107 );
108
109 logger.debug(
110 {
111 process: "ACCOUNT_THRESHOLD",
112 did,
113 labels,
114 count,
115 threshold: config.threshold,
116 window: config.window,
117 windowUnit: config.windowUnit,
118 },
119 "Checked account threshold",
120 );
121
122 if (count >= config.threshold) {
123 accountThresholdMetCounter.inc({ account_label: config.accountLabel });
124
125 logger.info(
126 {
127 process: "ACCOUNT_THRESHOLD",
128 did,
129 postLabel,
130 accountLabel: config.accountLabel,
131 count,
132 threshold: config.threshold,
133 },
134 "Account threshold met",
135 );
136
137 const shouldLabel = config.toLabel !== false;
138
139 const formattedComment = `${config.accountComment}\n\nThreshold: ${count.toString()}/${config.threshold.toString()} in ${config.window.toString()} ${config.windowUnit}\n\nPost: ${uri}\n\nPost Label: ${postLabel}`;
140
141 if (shouldLabel) {
142 await createAccountLabel(did, config.accountLabel, formattedComment);
143 accountLabelsThresholdAppliedCounter.inc({
144 account_label: config.accountLabel,
145 action: "label",
146 });
147 }
148
149 if (config.reportAcct) {
150 await createAccountReport(did, formattedComment);
151 accountLabelsThresholdAppliedCounter.inc({
152 account_label: config.accountLabel,
153 action: "report",
154 });
155 }
156
157 if (config.commentAcct) {
158 const atURI = `threshold-comment:${config.accountLabel}:${timestamp.toString()}`;
159 await createAccountComment(did, formattedComment, atURI);
160 accountLabelsThresholdAppliedCounter.inc({
161 account_label: config.accountLabel,
162 action: "comment",
163 });
164 }
165 }
166 }
167 } catch (error) {
168 logger.error(
169 { process: "ACCOUNT_THRESHOLD", did, postLabel, error },
170 "Error checking account threshold",
171 );
172 throw error;
173 }
174}