A tool for parsing traffic on the jetstream and applying a moderation workstream based on regexp based rules
7
fork

Configure Feed

Select the types of activity you want to include in your feed.

at postTracking 213 lines 6.3 kB view raw
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();