A tool for parsing traffic on the jetstream and applying a moderation workstream based on regexp based rules
1import "dotenv/config";
2import fs from "node:fs";
3import path from "node:path";
4import { fileURLToPath } from "node:url";
5import type { TrackedLabelConfig } from "./types.js";
6
7const __filename = fileURLToPath(import.meta.url);
8const __dirname = path.dirname(__filename);
9
10export const MOD_DID = process.env.DID ?? "";
11export const OZONE_URL = process.env.OZONE_URL ?? "";
12export const OZONE_PDS = process.env.OZONE_PDS ?? "";
13export const BSKY_HANDLE = process.env.BSKY_HANDLE ?? "";
14export const BSKY_PASSWORD = process.env.BSKY_PASSWORD ?? "";
15export const HOST = process.env.HOST ?? "127.0.0.1";
16export const PORT = process.env.PORT ? Number(process.env.PORT) : 4100;
17export const METRICS_PORT = process.env.METRICS_PORT
18 ? Number(process.env.METRICS_PORT)
19 : 4101; // Left this intact from the code I adapted this from
20export const FIREHOSE_URL =
21 process.env.FIREHOSE_URL ?? "wss://jetstream.atproto.tools/subscribe";
22export const PLC_URL = process.env.PLC_URL ?? "plc.directory";
23export const REDIS_URL = process.env.REDIS_URL ?? "redis://localhost:6379";
24export const WANTED_COLLECTION = [
25 "app.bsky.feed.post",
26 "app.bsky.actor.defs",
27 "app.bsky.actor.profile",
28];
29export const CURSOR_UPDATE_INTERVAL = process.env.CURSOR_UPDATE_INTERVAL
30 ? Number(process.env.CURSOR_UPDATE_INTERVAL)
31 : 60000;
32export const LABEL_LIMIT = process.env.LABEL_LIMIT;
33export const LABEL_LIMIT_WAIT = process.env.LABEL_LIMIT_WAIT;
34
35/**
36 * Validate a single tracked label configuration
37 */
38export function validateTrackedLabelConfig(
39 config: unknown,
40 index: number,
41): config is TrackedLabelConfig {
42 if (!config || typeof config !== "object" || Array.isArray(config)) {
43 throw new Error(
44 `Configuration at index ${index} is not an object: ${JSON.stringify(config)}`,
45 );
46 }
47
48 const c = config as Record<string, unknown>;
49
50 // Required fields
51 // Label can be a string or array of strings
52 if (typeof c.label === "string") {
53 if (c.label.trim() === "") {
54 throw new Error(
55 `Configuration at index ${index} has invalid 'label': string cannot be empty`,
56 );
57 }
58 } else if (Array.isArray(c.label)) {
59 if (c.label.length === 0) {
60 throw new Error(
61 `Configuration at index ${index} has invalid 'label': array cannot be empty`,
62 );
63 }
64 for (let i = 0; i < c.label.length; i++) {
65 if (typeof c.label[i] !== "string" || c.label[i].trim() === "") {
66 throw new Error(
67 `Configuration at index ${index} has invalid 'label[${i}]': must be a non-empty string`,
68 );
69 }
70 }
71 } else {
72 throw new Error(
73 `Configuration at index ${index} has invalid 'label': must be a string or array of strings`,
74 );
75 }
76
77 if (typeof c.threshold !== "number") {
78 throw new Error(
79 `Configuration at index ${index} has invalid 'threshold': must be a number`,
80 );
81 }
82
83 if (c.threshold <= 0) {
84 throw new Error(
85 `Configuration at index ${index} has invalid 'threshold': must be greater than 0 (got ${c.threshold})`,
86 );
87 }
88
89 if (!Number.isInteger(c.threshold)) {
90 throw new Error(
91 `Configuration at index ${index} has invalid 'threshold': must be an integer (got ${c.threshold})`,
92 );
93 }
94
95 if (typeof c.accountLabel !== "string" || c.accountLabel.trim() === "") {
96 throw new Error(
97 `Configuration at index ${index} has invalid 'accountLabel': must be a non-empty string`,
98 );
99 }
100
101 if (
102 typeof c.accountComment !== "string" ||
103 c.accountComment.trim() === ""
104 ) {
105 throw new Error(
106 `Configuration at index ${index} has invalid 'accountComment': must be a non-empty string`,
107 );
108 }
109
110 // Optional fields
111 if (c.windowDays !== undefined) {
112 if (typeof c.windowDays !== "number") {
113 throw new Error(
114 `Configuration at index ${index} has invalid 'windowDays': must be a number`,
115 );
116 }
117 if (c.windowDays <= 0) {
118 throw new Error(
119 `Configuration at index ${index} has invalid 'windowDays': must be greater than 0 (got ${c.windowDays})`,
120 );
121 }
122 if (!Number.isInteger(c.windowDays)) {
123 throw new Error(
124 `Configuration at index ${index} has invalid 'windowDays': must be an integer (got ${c.windowDays})`,
125 );
126 }
127 }
128
129 if (c.reportAcct !== undefined && typeof c.reportAcct !== "boolean") {
130 throw new Error(
131 `Configuration at index ${index} has invalid 'reportAcct': must be a boolean`,
132 );
133 }
134
135 if (c.commentAcct !== undefined && typeof c.commentAcct !== "boolean") {
136 throw new Error(
137 `Configuration at index ${index} has invalid 'commentAcct': must be a boolean`,
138 );
139 }
140
141 return true;
142}
143
144/**
145 * Load and validate tracked labels configuration from JSON file
146 */
147export function loadTrackedLabels(): TrackedLabelConfig[] {
148 const configPath = path.join(__dirname, "..", "tracked-labels.json");
149
150 // Check if file exists
151 if (!fs.existsSync(configPath)) {
152 console.error(
153 `FATAL: tracked-labels.json not found at ${configPath}`,
154 );
155 console.error(
156 "Create this file using tracked-labels.example.json as a template",
157 );
158 process.exit(1);
159 }
160
161 // Read file
162 let fileContent: string;
163 try {
164 fileContent = fs.readFileSync(configPath, "utf-8");
165 } catch (error) {
166 console.error(
167 `FATAL: Failed to read tracked-labels.json: ${error}`,
168 );
169 process.exit(1);
170 }
171
172 // Parse JSON
173 let parsed: unknown;
174 try {
175 parsed = JSON.parse(fileContent);
176 } catch (error) {
177 console.error(
178 `FATAL: tracked-labels.json contains invalid JSON: ${error}`,
179 );
180 process.exit(1);
181 }
182
183 // Validate it's an array
184 if (!Array.isArray(parsed)) {
185 console.error(
186 `FATAL: tracked-labels.json must contain an array, got ${typeof parsed}`,
187 );
188 process.exit(1);
189 }
190
191 // Validate each config
192 try {
193 parsed.forEach((config, index) => {
194 validateTrackedLabelConfig(config, index);
195 });
196 } catch (error) {
197 console.error(
198 `FATAL: Invalid tracked label configuration: ${error}`,
199 );
200 process.exit(1);
201 }
202
203 console.log(
204 `✓ Loaded ${parsed.length} tracked label configuration(s)`,
205 );
206
207 return parsed as TrackedLabelConfig[];
208}
209
210/**
211 * Tracked labels configuration (loaded at startup)
212 */
213export const TRACKED_LABELS: TrackedLabelConfig[] = loadTrackedLabels();