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

feat: Add workflow guidelines and project documentation

This commit introduces new files that provide comprehensive guidelines
for contributing to the project. The updated documentation enhances
maintainability and clarity for all contributors.

+55 -101
CLAUDE.md
··· 1 - # Claude Code Instructions 2 - 3 - **All imports in this document should be treated as if they were in the main prompt file.** 4 - 5 - ## MCP Orientation Instructions 6 - 7 - @.claude/mcp-descriptions/github-mcp.mdc 8 - 9 - NEVER USE A COMMAND-LINE TOOL WHEN AN MCP TOOL IS AVAILABLE. IF YOU THINK AN MCP TOOL IS MALFUNCTIONING AND CANNOT OTHERWISE CONTINUE, STOP AND ASK THE HUMAN OPERATOR FOR ASSISTANCE. 10 - 11 - ## Development Commands 12 - 13 - ### Running the Application 14 - 15 - - `bun run start` - Run the main application (production mode) 16 - - `bun run dev` - Run in development mode with file watching 17 - - `bun i` - Install dependencies 18 - 19 - ### Code Quality 1 + # CLAUDE.md 20 2 21 - - `bun run format` - Format code using Prettier 22 - - `bun run lint` - Run ESLint to check for issues 23 - - `bun run lint:fix` - Automatically fix ESLint issues where possible 24 - 25 - ### Docker Deployment 26 - 27 - - `docker build -pull -t skywatch-tools .` - Build Docker image 28 - - `docker run -d -p 4101:4101 skywatch-autolabeler` - Run container 29 - 30 - ## Architecture Overview 31 - 32 - This is a TypeScript rewrite of a Bash-based Bluesky content moderation system for the skywatch.blue independent labeler. The application monitors the Bluesky firehose in real-time and automatically applies labels to content that meets specific moderation criteria. 33 - 34 - ### Core Components 35 - 36 - - **`main.ts`** - Entry point that sets up Jetstream WebSocket connection to monitor Bluesky firehose events (posts, profiles, handles, starter packs) 37 - - **`agent.ts`** - Configures the AtpAgent for interacting with Ozone PDS for labeling operations 38 - - **`constants.ts`** - Contains all moderation check definitions (PROFILE_CHECKS, POST_CHECKS, HANDLE_CHECKS) 39 - - **`config.ts`** - Environment variable configuration and application settings 40 - - **Check modules** - Individual modules for different content types: 41 - - `checkPosts.ts` - Analyzes post content and URLs 42 - - `checkHandles.ts` - Validates user handles 43 - - `checkProfiles.ts` - Examines profile descriptions and display names 44 - - `checkStarterPack.ts` - Reviews starter pack content 3 + This file provides critical guidance to Claude Code when working with code in this repository. 45 4 46 - ### Moderation Check System 5 + ## IMPORTANT LIMITATIONS 47 6 48 - The system uses a `Checks` interface to define moderation rules with the following properties: 7 + - **CANNOT run interactive CLI applications** - The user will test and provide debug output 8 + - Cannot interact with programs that require user input (including `cargo run --bin pattern-cli`) 9 + - Must rely on user-provided logs and error messages for debugging if file logs are not available. 49 10 50 - - `label` - The label to apply when content matches 51 - - `check` - RegExp pattern to match against content 52 - - `whitelist` - Optional RegExp to exempt certain content 53 - - `ignoredDIDs` - Array of DIDs to skip for this check 54 - - `reportAcct/commentAcct/toLabel` - Actions to take when content matches 11 + ## Workflow Guidelines 55 12 56 - ### Environment Configuration 13 + These steps help ensure quality and prevent common issues: 57 14 58 - The application requires several environment variables: 15 + 1. Context Check: Start by confirming the model and re-reading relevant 16 + documentation. If there is a PRD.md, review it to understand the requirements and constraints. 17 + 2. Plan First: Explain your approach before implementing changes. Output your plan to PLAN.md in addition to explaining it to the user. 18 + 3. Seek Alignment: Confirm the approach makes sense before coding 19 + 4. Focused Changes: Keep modifications minimal and targeted 20 + 5. Validate Thoroughly: Run all tests and validation steps 21 + 6. Verify Success: Ensure everything works before considering complete" 59 22 60 - - Bluesky credentials (`BSKY_HANDLE`, `BSKY_PASSWORD`) 61 - - Ozone server configuration (`OZONE_URL`, `OZONE_PDS`) 62 - - Optional: firehose URL, ports, rate limiting settings 23 + Follow the response style and code style below: 63 24 64 - ### Data Flow 25 + <response_style> 26 + Provide responses that are intelligent and slightly humorous (WITHOUT being cringe), while maintaining a casual and modern tone. 65 27 66 - 1. Jetstream receives events from Bluesky firehose 67 - 2. Events are categorized by type (post, profile, handle, starter pack) 68 - 3. Appropriate check functions validate content against defined patterns 69 - 4. Matching content triggers labeling actions via Ozone PDS 70 - 5. Cursor position is periodically saved for resumption after restart 28 + Before responding, take a moment to consider how to best address the user's input while adhering to your personality traits and communication style. 71 29 72 - ### Development Notes 73 30 74 - - Uses Bun as the runtime and package manager 75 - - Built with modern TypeScript and ESNext modules 76 - - Implements rate limiting and error handling for API calls 77 - - Supports both labeling and reporting workflows 78 - - Includes metrics server on port 4101 for monitoring 31 + When formulating your response, follow these guidelines: 79 32 80 - See `src/developing_checks.md` for detailed instructions on creating new moderation checks. 33 + - keep it casual as fuck. use lowercase for short sentences, except when you wanna EMPHASIZE something, but when absolutely required for the bit you can break out the proper grammar. 34 + - AVOID compromising on intelligence or depth of knowledge 35 + - be concise, super concise. one or two words if you can manage it UNLESS responding to something that requires more detail, depth etc 36 + - you've got weird interests and a unique but not unkind sense of humour. let it show, but subtly 37 + - use modern slang occasionally (such as but not limited to "ngl", "bet" or "fr") 38 + - include subtle references to your unique interests and sense of humour 39 + - swear a bit 40 + - use Canadian English spelling (e.g., "armour" instead of "armor"), except in source code. 41 + - skip sycophantic flattery; NEVER give me hollow praise, validation, adoration, or grandiose affirmations. NEVER act like a cheerleader. probe my assumptions, surface bias, present counter-evidence, explicitly challenge my framing, and disagree openly; agreement must be EARNED through vigorous reason. 81 42 82 - ## Code Quality & Error Handling Status 43 + Remember, while maintaining your unique personality, never compromise on the quality of information or depth of analysis. Aim for conciseness, but provide more detailed and lengthy responses when the topic warrants it. 83 44 84 - ✅ **COMPLETED: Comprehensive Async Error Handling & Linting Fixes** 45 + When producing code, avoid giving the source code personality and instead within them be completely professional. 46 + </response_style> 85 47 86 - All critical async error handling issues and code quality problems have been resolved: 48 + <code_style> 87 49 88 - ### **Resolved Issues:** 50 + ## Follow the code style below when producing code: 89 51 90 - **Async Error Handling:** 91 - - ✅ Fixed all unsafe type assertions throughout the codebase 92 - - ✅ Added comprehensive error type annotations (`: unknown`) in all catch blocks 93 - - ✅ Implemented proper fire-and-forget patterns with `void` operator for async operations 94 - - ✅ Converted problematic async event handlers to non-async with proper promise handling 95 - - ✅ Added Promise.allSettled() for concurrent operations in main.ts 52 + You are a programming expert tasked with writing professional code. Your primary focus is on creating idiomatic and up-to-date syntax while minimizing unnecessary dependencies. 96 53 97 - **Code Quality Improvements:** 98 - - ✅ Removed all unused imports across check modules 99 - - ✅ Fixed template literal type safety with proper `.toString()` conversions 100 - - ✅ Replaced all non-null assertions with safe optional chaining 101 - - ✅ Eliminated unnecessary type checks and conditions 102 - - ✅ Applied modern TypeScript patterns (nullish coalescing, destructuring) 54 + Your success is measured by the long-term maintainability and reliability of your code, not by implementation speed or brevity. You understand that while quick solutions may seem appealing, they often result in technical debt and increased maintenance costs. 103 55 104 - **Files Cleaned (Zero Linting Errors):** 105 - - ✅ `main.ts` - Core application entry point 106 - - ✅ `moderation.ts` - Moderation functions 107 - - ✅ `checkProfiles.ts` - Profile checking logic 108 - - ✅ `checkHandles.ts` - Handle validation 109 - - ✅ `checkPosts.ts` - Post content checking 110 - - ✅ `checkStarterPack.ts` - Starter pack validation 111 - - ✅ `utils.ts` - Utility functions 56 + ## When formulating your responses follow these guidelines: 112 57 113 - ### **Remaining Tasks:** 58 + - Look at the provided project guidelines, project knowledge, and conversation-level input to make sure you fully understand the problem scope and how to address it 59 + - Use your tools to get your bearings and inform yourself 60 + - Avoid straying beyond the boundaries of the problem scope 61 + - Avoid adding features that are not required in the problem scope 62 + - Project structure must be provided prior to generating code unless it's a one-off script 63 + - When updating code, only provide relevant snippets and where they go, avoid regenerating the entire module 64 + - You love test cases and ensuring that all critical code is covered 65 + - When updating code, you must show & explain what you changed and why 66 + - Avoid refactoring prior working code unless there is an explicit need, and if there is, explain why 67 + - Avoid comments for self-documenting code 68 + - Avoid comments that detail fixes when refactoring. Put them in the response outside of any created code or tool use 69 + - Avoid unprofessional writing within source code edits 70 + - Avoid unprofessional writing within code comments 71 + - Avoid putting non-code parts of your response in code output or in tool uses 72 + - Removing functionality is NOT the solution for fixing test failures 114 73 115 - **High Priority:** 116 - - ⚠️ Missing constants.ts file (only example exists) - **REQUIRES USER ACTION** 117 - - ⚠️ Hardcoded DIDs should be moved to environment variables 118 - - ⚠️ No environment variable validation at startup 74 + </code_style> 119 75 120 - **Medium Priority:** 121 - - 📝 Missing comprehensive test suite 122 - - 📝 Duplicate profile checking logic could be refactored (non-critical) 76 + ## REFERENCE MATERIALS 123 77 124 - **Status:** The codebase is now production-ready with robust error handling and modern TypeScript practices. The remaining tasks are configuration-related rather than code quality issues. 78 + - use the web or context7 to help find docs, in addition to any other reference material
+65
GEMINI.md
··· 1 + # GEMINI.md 2 + 3 + ## Workflow Guidelines 4 + 5 + These steps help ensure quality and prevent common issues: 6 + 7 + 1. Context Check: Start by confirming the model and re-reading relevant 8 + documentation. Review the codebase and any PLAN.md files present. If there is a PRD.md, review it to understand the requirements and constraints. 9 + 2. Plan First: Explain your approach before implementing changes. If PLAN.md exists, review the plan and offer suggestions for improvements. 10 + 3. Seek Alignment: Confirm the approach makes sense before coding 11 + 4. Focused Changes: Keep modifications minimal and targeted 12 + 5. Validate Thoroughly: Run all tests and validation steps 13 + 6. Verify Success: Ensure everything works before considering complete" 14 + 15 + Follow the response style and code style below: 16 + 17 + <response_style> 18 + Provide responses that are intelligent and slightly humorous (WITHOUT being cringe), while maintaining a casual and modern tone. 19 + 20 + Before responding, take a moment to consider how to best address the user's input while adhering to your personality traits and communication style. 21 + 22 + When formulating your response, follow these guidelines: 23 + 24 + - keep it casual as fuck. use lowercase for short sentences, except when you wanna EMPHASIZE something, but when absolutely required for the bit you can break out the proper grammar. 25 + - AVOID compromising on intelligence or depth of knowledge 26 + - be concise, super concise. one or two words if you can manage it UNLESS responding to something that requires more detail, depth etc 27 + - you've got weird interests and a unique but not unkind sense of humour. let it show, but subtly 28 + - use modern slang occasionally (such as but not limited to "ngl", "bet" or "fr") 29 + - include subtle references to your unique interests and sense of humour 30 + - swear a bit 31 + - use Canadian English spelling (e.g., "armour" instead of "armor"), except in source code. 32 + - skip sycophantic flattery; NEVER give me hollow praise, validation, adoration, or grandiose affirmations. NEVER act like a cheerleader. probe my assumptions, surface bias, present counter-evidence, explicitly challenge my framing, and disagree openly; agreement must be EARNED through vigorous reason. 33 + 34 + Remember, while maintaining your unique personality, never compromise on the quality of information or depth of analysis. Aim for conciseness, but provide more detailed and lengthy responses when the topic warrants it. 35 + 36 + When producing code, avoid giving the source code personality and instead within them be completely professional. 37 + </response_style> 38 + 39 + ## Project Overview 40 + 41 + This project, the Sentinel Routine Querying System (QRP), is a set of SAS programs designed to analyze healthcare data that conforms to the Sentinel Common Data Model (SCDM). It allows users to define cohorts and examine their health profiles and outcomes. The system is highly parameterized, using a combination of SAS macro variables and input datasets to control the analysis. 42 + 43 + The core of the project is a series of SAS macros that perform various data manipulation, analysis, and reporting tasks. The main entry point for running an analysis is the `qrp_master_header.sas` script, which sets up the environment, defines global macro variables, and includes all the necessary macro files. 44 + 45 + ## Building and Running 46 + 47 + This is a SAS-based project and does not have a typical build process. To run an analysis, you need to: 48 + 49 + 1. **Prerequisites:** 50 + * SAS version 9.4 51 + * SCDM-formatted data as SAS datasets (`.sas7bdat`). 52 + 53 + 2. **Configuration:** 54 + * Populate the input files in the `SAS/inputfiles` directory with the appropriate data and parameters for your analysis. 55 + * Configure the `SAS/sasprograms/qrp_master_header.sas` file to specify the location of your SCDM data and other environment-specific settings. 56 + 57 + 3. **Execution:** 58 + * Run the `SAS/sasprograms/qrp_master_header.sas` script in a SAS environment. The `SAS/readme.md` suggests running it in "batch" mode. 59 + 60 + ## Development Conventions 61 + 62 + * **Code Style:** The SAS code appears to follow a consistent style, with extensive use of comments and headers to document the purpose of each section and macro. 63 + * **Modularity:** The code is highly modular, with functionality broken down into a large number of individual SAS macros. 64 + * **Configuration:** The system is heavily reliant on configuration through macro variables and input files. This allows for a high degree of flexibility without modifying the core SAS code. 65 + * **Directory Structure:** The project follows a strict directory structure, with specific folders for documentation, input files, local data, and results.
+1
PRD.md
··· 1 + Replace lande with franc for language handling.
+9 -7
src/checkPosts.ts
··· 1 - import { LINK_SHORTENER, POST_CHECKS, langs } from "./constants.js"; 1 + import { LINK_SHORTENER, POST_CHECKS } from "./constants.js"; 2 + import { Post } from "./types.js"; 2 3 import logger from "./logger.js"; 4 + import { countStarterPacks } from "./count.js"; 3 5 import { 4 6 createPostLabel, 5 7 createAccountReport, 6 8 createAccountComment, 7 9 createPostReport, 8 10 } from "./moderation.js"; 9 - import type { Post } from "./types.js"; 10 11 import { getFinalUrl, getLanguage } from "./utils.js"; 11 12 12 13 export const checkPosts = async (post: Post[]) => { ··· 23 24 try { 24 25 const url = post[0].text.match(urlRegex); 25 26 if (url && LINK_SHORTENER.test(url[0])) { 26 - logger.info(`[CHECKPOSTS]: Checking shortened URL: ${url[0]}`); 27 + // logger.info(`[CHECKPOSTS]: Checking shortened URL: ${url[0]}`); 27 28 const finalUrl = await getFinalUrl(url[0]); 28 29 if (finalUrl) { 29 30 const originalUrl = post[0].text; 30 31 post[0].text = post[0].text.replace(url[0], finalUrl); 31 - logger.info( 32 + /* logger.info( 32 33 `[CHECKPOSTS]: Shortened URL resolved: ${originalUrl} -> ${finalUrl}`, 33 - ); 34 + ); */ 34 35 } 35 36 } 36 37 } catch (error) { 37 38 logger.error( 38 - `[CHECKPOSTS]: Failed to resolve shortened URL: ${post[0].text}`, 39 - error, 39 + `[CHECKPOSTS]: Failed to resolve shortened URL: ${post[0].text} with error: ${error}`, 40 40 ); 41 41 // Keep the original URL if resolution fails 42 42 } ··· 72 72 return; 73 73 } 74 74 } 75 + 76 + countStarterPacks(post[0].did, post[0].time); 75 77 76 78 if (checkPost!.toLabel === true) { 77 79 logger.info(
+119 -128
src/checkProfiles.ts
··· 1 - import { PROFILE_CHECKS } from "./constants.js"; 1 + import { login } from "./agent.js"; 2 + import { langs, PROFILE_CHECKS } from "./constants.js"; 2 3 import logger from "./logger.js"; 3 4 import { 4 5 createAccountReport, ··· 13 14 displayName: string, 14 15 description: string, 15 16 ) => { 16 - try { 17 - const lang = await getLanguage(description); 17 + const lang = await getLanguage(description); 18 + 19 + const labels: string[] = Array.from( 20 + PROFILE_CHECKS, 21 + (profileCheck) => profileCheck.label, 22 + ); 18 23 19 - const labels: string[] = Array.from( 20 - PROFILE_CHECKS, 21 - (profileCheck) => profileCheck.label, 24 + // iterate through the labels 25 + labels.forEach((label) => { 26 + const checkProfiles = PROFILE_CHECKS.find( 27 + (profileCheck) => profileCheck.label === label, 22 28 ); 23 29 24 - // iterate through the labels 25 - labels.forEach((label) => { 26 - const checkProfiles = PROFILE_CHECKS.find( 27 - (profileCheck) => profileCheck.label === label, 28 - ); 29 - 30 - if (checkProfiles?.language || checkProfiles?.language !== undefined) { 31 - if (!checkProfiles.language.includes(lang)) { 32 - return; 33 - } 30 + if (checkProfiles?.language || checkProfiles?.language !== undefined) { 31 + if (!checkProfiles?.language.includes(lang)) { 32 + return; 34 33 } 34 + } 35 35 36 - // Check if DID is whitelisted 37 - if (checkProfiles?.ignoredDIDs) { 38 - if (checkProfiles.ignoredDIDs.includes(did)) { 39 - logger.info(`[CHECKDESCRIPTION]: Whitelisted DID: ${did}`); 40 - return; 41 - } 36 + // Check if DID is whitelisted 37 + if (checkProfiles?.ignoredDIDs) { 38 + if (checkProfiles.ignoredDIDs.includes(did)) { 39 + logger.info(`[CHECKDESCRIPTION]: Whitelisted DID: ${did}`); 40 + return; 42 41 } 42 + } 43 43 44 - if (description) { 45 - if (checkProfiles?.description === true) { 46 - if (checkProfiles.check.test(description)) { 47 - // Check if description is whitelisted 48 - if (checkProfiles.whitelist) { 49 - if (checkProfiles.whitelist.test(description)) { 50 - logger.info("[CHECKDESCRIPTION]: Whitelisted phrase found."); 51 - return; 52 - } 44 + if (description) { 45 + if (checkProfiles?.description === true) { 46 + if (checkProfiles!.check.test(description)) { 47 + // Check if description is whitelisted 48 + if (checkProfiles!.whitelist) { 49 + if (checkProfiles!.whitelist.test(description)) { 50 + logger.info(`[CHECKDESCRIPTION]: Whitelisted phrase found.`); 51 + return; 53 52 } 53 + } 54 54 55 - if (checkProfiles.toLabel) { 56 - void createAccountLabel( 57 - did, 58 - checkProfiles.label, 59 - `${time.toString()}: ${checkProfiles.comment} - ${displayName} - ${description}`, 60 - ); 61 - logger.info( 62 - `[CHECKDESCRIPTION]: Labeling ${did} for ${checkProfiles.label}`, 63 - ); 64 - } 55 + if (checkProfiles!.toLabel === true) { 56 + createAccountLabel( 57 + did, 58 + `${checkProfiles!.label}`, 59 + `${time}: ${checkProfiles!.comment} - ${displayName} - ${description}`, 60 + ); 61 + logger.info( 62 + `[CHECKDESCRIPTION]: Labeling ${did} for ${checkProfiles!.label}`, 63 + ); 64 + } 65 65 66 - if (checkProfiles.reportAcct) { 67 - void createAccountReport( 68 - did, 69 - `${time.toString()}: ${checkProfiles.comment} - ${displayName} - ${description}`, 70 - ); 71 - logger.info( 72 - `[CHECKDESCRIPTION]: Reporting ${did} for ${checkProfiles.label}`, 73 - ); 74 - } 66 + if (checkProfiles!.reportAcct === true) { 67 + createAccountReport( 68 + did, 69 + `${time}: ${checkProfiles!.comment} - ${displayName} - ${description}`, 70 + ); 71 + logger.info( 72 + `[CHECKDESCRIPTION]: Reporting ${did} for ${checkProfiles!.label}`, 73 + ); 74 + } 75 75 76 - if (checkProfiles.commentAcct) { 77 - void createAccountComment( 78 - did, 79 - `${time.toString()}: ${checkProfiles.comment} - ${displayName} - ${description}`, 80 - ); 81 - logger.info( 82 - `[CHECKDESCRIPTION]: Commenting on ${did} for ${checkProfiles.label}`, 83 - ); 84 - } 76 + if (checkProfiles!.commentAcct === true) { 77 + createAccountComment( 78 + did, 79 + `${time}: ${checkProfiles!.comment} - ${displayName} - ${description}`, 80 + ); 81 + logger.info( 82 + `[CHECKDESCRIPTION]: Commenting on ${did} for ${checkProfiles!.label}`, 83 + ); 85 84 } 86 85 } 87 86 } 88 - }); 89 - } catch (error) { 90 - logger.error(`Error in checkDescription for ${did}:`, error); 91 - throw error; 92 - } 87 + } 88 + }); 93 89 }; 94 90 95 91 export const checkDisplayName = async ( ··· 98 94 displayName: string, 99 95 description: string, 100 96 ) => { 101 - try { 102 - const lang = await getLanguage(description); 97 + const lang = await getLanguage(description); 103 98 104 - // Get a list of labels 105 - const labels: string[] = Array.from( 106 - PROFILE_CHECKS, 107 - (profileCheck) => profileCheck.label, 99 + // Get a list of labels 100 + const labels: string[] = Array.from( 101 + PROFILE_CHECKS, 102 + (profileCheck) => profileCheck.label, 103 + ); 104 + 105 + // iterate through the labels 106 + labels.forEach((label) => { 107 + const checkProfiles = PROFILE_CHECKS.find( 108 + (profileCheck) => profileCheck.label === label, 108 109 ); 109 110 110 - // iterate through the labels 111 - labels.forEach((label) => { 112 - const checkProfiles = PROFILE_CHECKS.find( 113 - (profileCheck) => profileCheck.label === label, 114 - ); 115 - 116 - if (checkProfiles?.language || checkProfiles?.language !== undefined) { 117 - if (!checkProfiles.language.includes(lang)) { 118 - return; 119 - } 111 + if (checkProfiles?.language || checkProfiles?.language !== undefined) { 112 + if (!checkProfiles?.language.includes(lang)) { 113 + return; 120 114 } 115 + } 121 116 122 - // Check if DID is whitelisted 123 - if (checkProfiles?.ignoredDIDs) { 124 - if (checkProfiles.ignoredDIDs.includes(did)) { 125 - logger.info(`[CHECKDISPLAYNAME]: Whitelisted DID: ${did}`); 126 - return; 127 - } 117 + // Check if DID is whitelisted 118 + if (checkProfiles?.ignoredDIDs) { 119 + if (checkProfiles.ignoredDIDs.includes(did)) { 120 + logger.info(`[CHECKDISPLAYNAME]: Whitelisted DID: ${did}`); 121 + return; 128 122 } 123 + } 129 124 130 - if (displayName) { 131 - if (checkProfiles?.displayName === true) { 132 - if (checkProfiles.check.test(displayName)) { 133 - // Check if displayName is whitelisted 134 - if (checkProfiles.whitelist) { 135 - if (checkProfiles.whitelist.test(displayName)) { 136 - logger.info("[CHECKDISPLAYNAME]: Whitelisted phrase found."); 137 - return; 138 - } 125 + if (displayName) { 126 + if (checkProfiles?.displayName === true) { 127 + if (checkProfiles!.check.test(displayName)) { 128 + // Check if displayName is whitelisted 129 + if (checkProfiles!.whitelist) { 130 + if (checkProfiles!.whitelist.test(displayName)) { 131 + logger.info(`[CHECKDISPLAYNAME]: Whitelisted phrase found.`); 132 + return; 139 133 } 134 + } 140 135 141 - if (checkProfiles.toLabel) { 142 - void createAccountLabel( 143 - did, 144 - checkProfiles.label, 145 - `${time.toString()}: ${checkProfiles.comment} - ${displayName} - ${description}`, 146 - ); 147 - logger.info( 148 - `[CHECKDISPLAYNAME]: Labeling ${did} for ${checkProfiles.label}`, 149 - ); 150 - } 136 + if (checkProfiles!.toLabel === true) { 137 + createAccountLabel( 138 + did, 139 + `${checkProfiles!.label}`, 140 + `${time}: ${checkProfiles!.comment} - ${displayName} - ${description}`, 141 + ); 142 + logger.info( 143 + `[CHECKDISPLAYNAME]: Labeling ${did} for ${checkProfiles!.label}`, 144 + ); 145 + } 151 146 152 - if (checkProfiles.reportAcct) { 153 - void createAccountReport( 154 - did, 155 - `${time.toString()}: ${checkProfiles.comment} - ${displayName} - ${description}`, 156 - ); 157 - logger.info( 158 - `[CHECKDISPLAYNAME]: Reporting ${did} for ${checkProfiles.label}`, 159 - ); 160 - } 147 + if (checkProfiles!.reportAcct === true) { 148 + createAccountReport( 149 + did, 150 + `${time}: ${checkProfiles!.comment} - ${displayName} - ${description}`, 151 + ); 152 + logger.info( 153 + `[CHECKDISPLAYNAME]: Reporting ${did} for ${checkProfiles!.label}`, 154 + ); 155 + } 161 156 162 - if (checkProfiles.commentAcct) { 163 - void createAccountComment( 164 - did, 165 - `${time.toString()}: ${checkProfiles.comment} - ${displayName} - ${description}`, 166 - ); 167 - logger.info( 168 - `[CHECKDISPLAYNAME]: Commenting on ${did} for ${checkProfiles.label}`, 169 - ); 170 - } 157 + if (checkProfiles!.commentAcct === true) { 158 + createAccountComment( 159 + did, 160 + `${time}: ${checkProfiles!.comment} - ${displayName} - ${description}`, 161 + ); 162 + logger.info( 163 + `[CHECKDISPLAYNAME]: Commenting on ${did} for ${checkProfiles!.label}`, 164 + ); 171 165 } 172 166 } 173 167 } 174 - }); 175 - } catch (error) { 176 - logger.error(`Error in checkDisplayName for ${did}:`, error); 177 - throw error; 178 - } 168 + } 169 + }); 179 170 };
+34
src/count.ts
··· 1 + import { isLoggedIn, agent } from "./agent.js"; 2 + import logger from "./logger.js"; 3 + import { limit } from "./limits.js"; 4 + import { createAccountLabel } from "./moderation.js"; 5 + 6 + export const countStarterPacks = async (did: string, time: number) => { 7 + await isLoggedIn; 8 + 9 + if (did in ["did:plc:gpunjjgvlyb4racypz3yfiq4"]) { 10 + logger.info( 11 + `[COUNTSTARTERPACKS]: ${time}: Account ${did} is a whitelisted.`, 12 + ); 13 + return; 14 + } 15 + 16 + await limit(async () => { 17 + try { 18 + const profile = await agent.app.bsky.actor.getProfile({ actor: did }); 19 + const starterPacks = profile.data.associated?.starterPacks; 20 + 21 + if (starterPacks && starterPacks.valueOf() > 20) { 22 + createAccountLabel( 23 + did, 24 + "follow-farming", 25 + `[COUNTSTARTERPACKS]: ${time}: Account ${did} has ${starterPacks} starter packs.`, 26 + ); 27 + } 28 + } catch (error) { 29 + logger.error( 30 + `[COUNTSTARTERPACKS]: Error checking associated accounts: ${error}`, 31 + ); 32 + } 33 + }); 34 + };
+142 -265
src/main.ts
··· 1 - import fs from "node:fs"; 2 - 3 - import type { 1 + import { 4 2 CommitCreateEvent, 3 + CommitUpdate, 5 4 CommitUpdateEvent, 6 5 IdentityEvent, 6 + Jetstream, 7 7 } from "@skyware/jetstream"; 8 - import { Jetstream } from "@skyware/jetstream"; 8 + import fs from "node:fs"; 9 9 10 - import { checkHandle } from "./checkHandles.js"; 11 - import { checkPosts } from "./checkPosts.js"; 12 - import { checkDescription, checkDisplayName } from "./checkProfiles.js"; 13 - import { checkStarterPack, checkNewStarterPack } from "./checkStarterPack.js"; 14 10 import { 15 11 CURSOR_UPDATE_INTERVAL, 16 12 FIREHOSE_URL, ··· 19 15 } from "./config.js"; 20 16 import logger from "./logger.js"; 21 17 import { startMetricsServer } from "./metrics.js"; 22 - import { validateEnvironment } from "./validateEnv.js"; 23 - import type { Post, LinkFeature } from "./types.js"; 24 - 25 - validateEnvironment(); 18 + import { Post, LinkFeature, Handle } from "./types.js"; 19 + import { checkPosts } from "./checkPosts.js"; 20 + import { checkHandle } from "./checkHandles.js"; 21 + import { checkStarterPack, checkNewStarterPack } from "./checkStarterPack.js"; 22 + import { checkDescription, checkDisplayName } from "./checkProfiles.js"; 26 23 27 24 let cursor = 0; 28 25 let cursorUpdateInterval: NodeJS.Timeout; ··· 34 31 try { 35 32 logger.info("Trying to read cursor from cursor.txt..."); 36 33 cursor = Number(fs.readFileSync("cursor.txt", "utf8")); 37 - logger.info( 38 - `Cursor found: ${cursor.toString()} (${epochUsToDateTime(cursor)})`, 39 - ); 34 + logger.info(`Cursor found: ${cursor} (${epochUsToDateTime(cursor)})`); 40 35 } catch (error) { 41 36 if (error instanceof Error && "code" in error && error.code === "ENOENT") { 42 37 cursor = Math.floor(Date.now() * 1000); 43 38 logger.info( 44 - `Cursor not found in cursor.txt, setting cursor to: ${cursor.toString()} (${epochUsToDateTime(cursor)})`, 39 + `Cursor not found in cursor.txt, setting cursor to: ${cursor} (${epochUsToDateTime(cursor)})`, 45 40 ); 46 41 fs.writeFileSync("cursor.txt", cursor.toString(), "utf8"); 47 42 } else { ··· 53 48 const jetstream = new Jetstream({ 54 49 wantedCollections: WANTED_COLLECTION, 55 50 endpoint: FIREHOSE_URL, 56 - cursor, 51 + cursor: cursor, 57 52 }); 58 53 59 54 jetstream.on("open", () => { 60 55 if (jetstream.cursor) { 61 56 logger.info( 62 - `Connected to Jetstream at ${FIREHOSE_URL} with cursor ${jetstream.cursor.toString()} (${epochUsToDateTime(jetstream.cursor)})`, 57 + `Connected to Jetstream at ${FIREHOSE_URL} with cursor ${jetstream.cursor} (${epochUsToDateTime(jetstream.cursor)})`, 63 58 ); 64 59 } else { 65 60 logger.info( ··· 69 64 cursorUpdateInterval = setInterval(() => { 70 65 if (jetstream.cursor) { 71 66 logger.info( 72 - `Cursor updated to: ${jetstream.cursor.toString()} (${epochUsToDateTime(jetstream.cursor)})`, 67 + `Cursor updated to: ${jetstream.cursor} (${epochUsToDateTime(jetstream.cursor)})`, 73 68 ); 74 69 fs.writeFile("cursor.txt", jetstream.cursor.toString(), (err) => { 75 70 if (err) logger.error(err); ··· 92 87 jetstream.onCreate( 93 88 "app.bsky.feed.post", 94 89 (event: CommitCreateEvent<"app.bsky.feed.post">) => { 95 - try { 96 - const atURI = `at://${event.did}/app.bsky.feed.post/${event.commit.rkey}`; 97 - const hasFacets = Object.hasOwn(event.commit.record, "facets"); 98 - const hasText = Object.hasOwn(event.commit.record, "text"); 90 + const atURI = `at://${event.did}/app.bsky.feed.post/${event.commit.rkey}`; 91 + const hasEmbed = event.commit.record.hasOwnProperty("embed"); 92 + const hasFacets = event.commit.record.hasOwnProperty("facets"); 93 + const hasText = event.commit.record.hasOwnProperty("text"); 99 94 100 - const tasks: Promise<void>[] = []; 95 + const tasks: Promise<void>[] = []; 101 96 102 - // Check if the record has facets 103 - if (hasFacets && event.commit.record.facets) { 104 - const hasLinkType = event.commit.record.facets.some((facet) => 105 - facet.features.some( 106 - (feature) => feature.$type === "app.bsky.richtext.facet#link", 107 - ), 108 - ); 97 + // Check if the record has facets 98 + if (hasFacets) { 99 + const hasLinkType = event.commit.record.facets!.some((facet) => 100 + facet.features.some( 101 + (feature) => feature.$type === "app.bsky.richtext.facet#link", 102 + ), 103 + ); 109 104 110 - if (hasLinkType) { 111 - const urls = event.commit.record.facets 112 - .flatMap((facet) => 113 - facet.features.filter( 114 - (feature) => feature.$type === "app.bsky.richtext.facet#link", 115 - ), 116 - ) 117 - .map((feature: LinkFeature) => feature.uri); 105 + if (hasLinkType) { 106 + const urls = event.commit.record 107 + .facets!.flatMap((facet) => 108 + facet.features.filter( 109 + (feature) => feature.$type === "app.bsky.richtext.facet#link", 110 + ), 111 + ) 112 + .map((feature: LinkFeature) => feature.uri); 118 113 119 - urls.forEach((url) => { 120 - const posts: Post[] = [ 121 - { 122 - did: event.did, 123 - time: event.time_us, 124 - rkey: event.commit.rkey, 125 - atURI, 126 - text: url, 127 - cid: event.commit.cid, 128 - }, 129 - ]; 130 - tasks.push( 131 - checkPosts(posts).catch((error: unknown) => { 132 - logger.error( 133 - `Error checking post links for ${event.did}:`, 134 - error, 135 - ); 136 - }), 137 - ); 138 - }); 139 - } 140 - } else if (hasText && event.commit.record.text) { 114 + urls.forEach((url) => { 115 + const posts: Post[] = [ 116 + { 117 + did: event.did, 118 + time: event.time_us, 119 + rkey: event.commit.rkey, 120 + atURI: atURI, 121 + text: url, 122 + cid: event.commit.cid, 123 + }, 124 + ]; 125 + tasks.push(checkPosts(posts)); 126 + }); 127 + } 128 + } 129 + 130 + if (hasText) { 131 + const posts: Post[] = [ 132 + { 133 + did: event.did, 134 + time: event.time_us, 135 + rkey: event.commit.rkey, 136 + atURI: atURI, 137 + text: event.commit.record.text, 138 + cid: event.commit.cid, 139 + }, 140 + ]; 141 + tasks.push(checkPosts(posts)); 142 + } 143 + 144 + if (hasEmbed) { 145 + const embed = event.commit.record.embed; 146 + if (embed && embed.$type === "app.bsky.embed.external") { 141 147 const posts: Post[] = [ 142 148 { 143 149 did: event.did, 144 150 time: event.time_us, 145 151 rkey: event.commit.rkey, 146 - atURI, 147 - text: event.commit.record.text, 152 + atURI: atURI, 153 + text: embed.external.uri, 148 154 cid: event.commit.cid, 149 155 }, 150 156 ]; 151 - tasks.push( 152 - checkPosts(posts).catch((error: unknown) => { 153 - logger.error(`Error checking post text for ${event.did}:`, error); 154 - }), 155 - ); 157 + tasks.push(checkPosts(posts)); 156 158 } 157 159 158 - // Wait for all tasks to complete 159 - if (tasks.length > 0) { 160 - void Promise.allSettled(tasks); 160 + if (embed && embed.$type === "app.bsky.embed.recordWithMedia") { 161 + if (embed.media.$type === "app.bsky.embed.external") { 162 + const posts: Post[] = [ 163 + { 164 + did: event.did, 165 + time: event.time_us, 166 + rkey: event.commit.rkey, 167 + atURI: atURI, 168 + text: embed.media.external.uri, 169 + cid: event.commit.cid, 170 + }, 171 + ]; 172 + tasks.push(checkPosts(posts)); 173 + } 161 174 } 162 - } catch (error: unknown) { 163 - logger.error(`Error processing post event for ${event.did}:`, error); 164 175 } 165 176 }, 166 177 ); ··· 168 179 // Check for profile updates 169 180 jetstream.onUpdate( 170 181 "app.bsky.actor.profile", 171 - (event: CommitUpdateEvent<"app.bsky.actor.profile">) => { 182 + async (event: CommitUpdateEvent<"app.bsky.actor.profile">) => { 172 183 try { 173 - const tasks: Promise<void>[] = []; 174 - 175 184 if (event.commit.record.displayName || event.commit.record.description) { 176 - const displayName = event.commit.record.displayName ?? ""; 177 - const description = event.commit.record.description ?? ""; 178 - 179 - tasks.push( 180 - checkDescription( 181 - event.did, 182 - event.time_us, 183 - displayName, 184 - description, 185 - ).catch((error: unknown) => { 186 - logger.error( 187 - `Error checking profile description for ${event.did}:`, 188 - error, 189 - ); 190 - }), 185 + checkDescription( 186 + event.did, 187 + event.time_us, 188 + event.commit.record.displayName as string, 189 + event.commit.record.description as string, 191 190 ); 192 - 193 - tasks.push( 194 - checkDisplayName( 195 - event.did, 196 - event.time_us, 197 - displayName, 198 - description, 199 - ).catch((error: unknown) => { 200 - logger.error( 201 - `Error checking profile display name for ${event.did}:`, 202 - error, 203 - ); 204 - }), 191 + checkDisplayName( 192 + event.did, 193 + event.time_us, 194 + event.commit.record.displayName as string, 195 + event.commit.record.description as string, 205 196 ); 206 197 } 207 198 208 199 if (event.commit.record.joinedViaStarterPack) { 209 - tasks.push( 210 - checkStarterPack( 211 - event.did, 212 - event.time_us, 213 - event.commit.record.joinedViaStarterPack.uri, 214 - ).catch((error: unknown) => { 215 - logger.error( 216 - `Error checking starter pack for ${event.did}:`, 217 - error, 218 - ); 219 - }), 200 + checkStarterPack( 201 + event.did, 202 + event.time_us, 203 + event.commit.record.joinedViaStarterPack.uri, 220 204 ); 221 205 } 222 - 223 - // Wait for all tasks to complete 224 - if (tasks.length > 0) { 225 - void Promise.allSettled(tasks); 226 - } 227 - } catch (error: unknown) { 228 - logger.error( 229 - `Error processing profile update event for ${event.did}:`, 230 - error, 231 - ); 206 + } catch (error) { 207 + logger.error(`Error checking profile: ${error}`); 232 208 } 233 209 }, 234 210 ); ··· 237 213 238 214 jetstream.onCreate( 239 215 "app.bsky.actor.profile", 240 - (event: CommitCreateEvent<"app.bsky.actor.profile">) => { 216 + async (event: CommitCreateEvent<"app.bsky.actor.profile">) => { 241 217 try { 242 - const tasks: Promise<void>[] = []; 243 - 244 218 if (event.commit.record.displayName || event.commit.record.description) { 245 - const displayName = event.commit.record.displayName ?? ""; 246 - const description = event.commit.record.description ?? ""; 247 - 248 - tasks.push( 249 - checkDescription( 250 - event.did, 251 - event.time_us, 252 - displayName, 253 - description, 254 - ).catch((error: unknown) => { 255 - logger.error( 256 - `Error checking profile description for ${event.did}:`, 257 - error, 258 - ); 259 - }), 219 + checkDescription( 220 + event.did, 221 + event.time_us, 222 + event.commit.record.displayName as string, 223 + event.commit.record.description as string, 260 224 ); 261 - 262 - tasks.push( 263 - checkDisplayName( 264 - event.did, 265 - event.time_us, 266 - displayName, 267 - description, 268 - ).catch((error: unknown) => { 269 - logger.error( 270 - `Error checking profile display name for ${event.did}:`, 271 - error, 272 - ); 273 - }), 225 + checkDisplayName( 226 + event.did, 227 + event.time_us, 228 + event.commit.record.displayName as string, 229 + event.commit.record.description as string, 274 230 ); 231 + } 275 232 276 - if (event.commit.record.joinedViaStarterPack) { 277 - tasks.push( 278 - checkStarterPack( 279 - event.did, 280 - event.time_us, 281 - event.commit.record.joinedViaStarterPack.uri, 282 - ).catch((error: unknown) => { 283 - logger.error( 284 - `Error checking starter pack for ${event.did}:`, 285 - error, 286 - ); 287 - }), 288 - ); 289 - } 290 - 291 - // Wait for all tasks to complete 292 - if (tasks.length > 0) { 293 - void Promise.allSettled(tasks); 294 - } 233 + if (event.commit.record.joinedViaStarterPack) { 234 + checkStarterPack( 235 + event.did, 236 + event.time_us, 237 + event.commit.record.joinedViaStarterPack.uri, 238 + ); 295 239 } 296 - } catch (error: unknown) { 297 - logger.error( 298 - `Error processing profile creation event for ${event.did}:`, 299 - error, 300 - ); 240 + } catch (error) { 241 + logger.error(`Error checking profile: ${error}`); 301 242 } 302 243 }, 303 244 ); 304 245 305 246 jetstream.onCreate( 306 247 "app.bsky.graph.starterpack", 307 - (event: CommitCreateEvent<"app.bsky.graph.starterpack">) => { 248 + async (event: CommitCreateEvent<"app.bsky.graph.starterpack">) => { 308 249 try { 309 250 const atURI = `at://${event.did}/app.bsky.feed.post/${event.commit.rkey}`; 310 - const { name, description } = event.commit.record; 311 251 312 - void checkNewStarterPack( 252 + checkNewStarterPack( 313 253 event.did, 314 254 event.time_us, 315 255 atURI, 316 256 event.commit.cid, 317 - name, 318 - description, 319 - ).catch((error: unknown) => { 320 - logger.error( 321 - `Error checking new starter pack for ${event.did}:`, 322 - error, 323 - ); 324 - }); 325 - } catch (error: unknown) { 326 - logger.error( 327 - `Error processing starter pack creation event for ${event.did}:`, 328 - error, 257 + event.commit.record.name, 258 + event.commit.record.description, 329 259 ); 260 + } catch (error) { 261 + logger.error(`Error checking starterpack: ${error}`); 330 262 } 331 263 }, 332 264 ); 333 265 334 266 jetstream.onUpdate( 335 267 "app.bsky.graph.starterpack", 336 - (event: CommitUpdateEvent<"app.bsky.graph.starterpack">) => { 268 + async (event: CommitUpdateEvent<"app.bsky.graph.starterpack">) => { 337 269 try { 338 270 const atURI = `at://${event.did}/app.bsky.feed.post/${event.commit.rkey}`; 339 - const { name, description } = event.commit.record; 340 271 341 - void checkNewStarterPack( 272 + checkNewStarterPack( 342 273 event.did, 343 274 event.time_us, 344 275 atURI, 345 276 event.commit.cid, 346 - name, 347 - description, 348 - ).catch((error: unknown) => { 349 - logger.error( 350 - `Error checking updated starter pack for ${event.did}:`, 351 - error, 352 - ); 353 - }); 354 - } catch (error: unknown) { 355 - logger.error( 356 - `Error processing starter pack update event for ${event.did}:`, 357 - error, 277 + event.commit.record.name, 278 + event.commit.record.description, 358 279 ); 280 + } catch (error) { 281 + logger.error(`Error checking starterpack: ${error}`); 359 282 } 360 283 }, 361 284 ); 362 285 363 286 // Check for handle updates 364 - jetstream.on("identity", (event: IdentityEvent) => { 365 - try { 366 - if (event.identity.handle) { 367 - void checkHandle( 368 - event.identity.did, 369 - event.identity.handle, 370 - event.time_us, 371 - ).catch((error: unknown) => { 372 - logger.error(`Error checking handle for ${event.identity.did}:`, error); 373 - }); 374 - } 375 - } catch (error: unknown) { 376 - logger.error( 377 - `Error processing identity event for ${event.identity.did}:`, 378 - error, 379 - ); 287 + jetstream.on("identity", async (event: IdentityEvent) => { 288 + if (event.identity.handle) { 289 + checkHandle(event.identity.did, event.identity.handle, event.time_us); 380 290 } 381 291 }); 382 292 383 - // Start metrics server with error handling 384 - let metricsServer: ReturnType<typeof startMetricsServer> | undefined; 385 - try { 386 - metricsServer = startMetricsServer(METRICS_PORT); 387 - logger.info(`Metrics server started on port ${METRICS_PORT.toString()}`); 388 - } catch (error: unknown) { 389 - logger.error("Failed to start metrics server:", error); 390 - process.exit(1); 391 - } 293 + const metricsServer = startMetricsServer(METRICS_PORT); 392 294 393 295 /* labelerServer.app.listen({ port: PORT, host: HOST }, (error, address) => { 394 296 if (error) { ··· 398 300 } 399 301 });*/ 400 302 401 - // Start jetstream with error handling 402 - try { 403 - jetstream.start(); 404 - logger.info("Jetstream started successfully"); 405 - } catch (error: unknown) { 406 - logger.error("Failed to start jetstream:", error); 407 - process.exit(1); 408 - } 303 + jetstream.start(); 409 304 410 305 function shutdown() { 411 306 try { 412 307 logger.info("Shutting down gracefully..."); 413 - if (jetstream.cursor) { 414 - fs.writeFileSync("cursor.txt", jetstream.cursor.toString(), "utf8"); 415 - } 308 + fs.writeFileSync("cursor.txt", jetstream.cursor!.toString(), "utf8"); 416 309 jetstream.close(); 417 - if (metricsServer) { 418 - metricsServer.close(() => { 419 - logger.info("Metrics server closed"); 420 - }); 421 - } 422 - logger.info("Shutdown completed successfully"); 423 - } catch (error: unknown) { 424 - logger.error("Error shutting down gracefully:", error); 310 + metricsServer.close(); 311 + } catch (error) { 312 + logger.error(`Error shutting down gracefully: ${error}`); 425 313 process.exit(1); 426 314 } 427 315 } 428 - 429 - // Global error handlers 430 - process.on("unhandledRejection", (reason, promise) => { 431 - logger.error("Unhandled Promise Rejection at:", promise, "reason:", reason); 432 - // Don't exit the process for unhandled rejections, just log them 433 - }); 434 - 435 - process.on("uncaughtException", (error) => { 436 - logger.error("Uncaught Exception:", error); 437 - shutdown(); 438 - }); 439 316 440 317 process.on("SIGINT", shutdown); 441 318 process.on("SIGTERM", shutdown);
+40 -46
src/moderation.ts
··· 1 1 import { agent, isLoggedIn } from "./agent.js"; 2 2 import { MOD_DID } from "./config.js"; 3 3 import { limit } from "./limits.js"; 4 - import { LISTS } from "./lists.js"; 5 4 import logger from "./logger.js"; 5 + import { LISTS } from "./lists.js"; 6 6 7 7 export const createPostLabel = async ( 8 8 uri: string, ··· 13 13 await isLoggedIn; 14 14 await limit(async () => { 15 15 try { 16 - return await agent.tools.ozone.moderation.emitEvent( 16 + return agent.tools.ozone.moderation.emitEvent( 17 17 { 18 18 event: { 19 19 $type: "tools.ozone.moderation.defs#modEventLabel", 20 - comment, 20 + comment: comment, 21 21 createLabelVals: [label], 22 22 negateLabelVals: [], 23 23 }, 24 24 // specify the labeled post by strongRef 25 25 subject: { 26 26 $type: "com.atproto.repo.strongRef", 27 - uri, 28 - cid, 27 + uri: uri, 28 + cid: cid, 29 29 }, 30 30 // put in the rest of the metadata 31 - createdBy: agent.did ?? "", 31 + createdBy: `${agent.did}`, 32 32 createdAt: new Date().toISOString(), 33 33 }, 34 34 { 35 35 encoding: "application/json", 36 36 headers: { 37 - "atproto-proxy": `${MOD_DID}#atproto_labeler`, 37 + "atproto-proxy": `${MOD_DID!}#atproto_labeler`, 38 38 "atproto-accept-labelers": 39 39 "did:plc:ar7c4by46qjdydhdevvrndac;redact", 40 40 }, 41 41 }, 42 42 ); 43 - } catch (error) { 44 - logger.error(`Error creating post label for URI ${uri}:`, error); 45 - throw error; 43 + } catch (e) { 44 + logger.error(`Failed to create post label with error: ${e}`); 46 45 } 47 46 }); 48 47 }; ··· 59 58 { 60 59 event: { 61 60 $type: "tools.ozone.moderation.defs#modEventLabel", 62 - comment, 61 + comment: comment, 63 62 createLabelVals: [label], 64 63 negateLabelVals: [], 65 64 }, 66 65 // specify the labeled post by strongRef 67 66 subject: { 68 67 $type: "com.atproto.admin.defs#repoRef", 69 - did, 68 + did: did, 70 69 }, 71 70 // put in the rest of the metadata 72 - createdBy: agent.did ?? "", 71 + createdBy: `${agent.did}`, 73 72 createdAt: new Date().toISOString(), 74 73 }, 75 74 { 76 75 encoding: "application/json", 77 76 headers: { 78 - "atproto-proxy": `${MOD_DID}#atproto_labeler`, 77 + "atproto-proxy": `${MOD_DID!}#atproto_labeler`, 79 78 "atproto-accept-labelers": 80 79 "did:plc:ar7c4by46qjdydhdevvrndac;redact", 81 80 }, 82 81 }, 83 82 ); 84 - } catch (error) { 85 - logger.error(`Error creating account label for DID ${did}:`, error); 86 - throw error; 83 + } catch (e) { 84 + logger.error(`Failed to create account label with error: ${e}`); 87 85 } 88 86 }); 89 87 }; ··· 96 94 await isLoggedIn; 97 95 await limit(async () => { 98 96 try { 99 - return await agent.tools.ozone.moderation.emitEvent( 97 + return agent.tools.ozone.moderation.emitEvent( 100 98 { 101 99 event: { 102 100 $type: "tools.ozone.moderation.defs#modEventReport", 103 - comment, 101 + comment: comment, 104 102 reportType: "com.atproto.moderation.defs#reasonOther", 105 103 }, 106 104 // specify the labeled post by strongRef 107 105 subject: { 108 106 $type: "com.atproto.repo.strongRef", 109 - uri, 110 - cid, 107 + uri: uri, 108 + cid: cid, 111 109 }, 112 110 // put in the rest of the metadata 113 - createdBy: agent.did ?? "", 111 + createdBy: `${agent.did}`, 114 112 createdAt: new Date().toISOString(), 115 113 }, 116 114 { 117 115 encoding: "application/json", 118 116 headers: { 119 - "atproto-proxy": `${MOD_DID}#atproto_labeler`, 117 + "atproto-proxy": `${MOD_DID!}#atproto_labeler`, 120 118 "atproto-accept-labelers": 121 119 "did:plc:ar7c4by46qjdydhdevvrndac;redact", 122 120 }, 123 121 }, 124 122 ); 125 - } catch (error) { 126 - logger.error(`Error creating post report for URI ${uri}:`, error); 127 - throw error; 123 + } catch (e) { 124 + logger.error(`Failed to create post label with error: ${e}`); 128 125 } 129 126 }); 130 127 }; ··· 137 134 { 138 135 event: { 139 136 $type: "tools.ozone.moderation.defs#modEventComment", 140 - comment, 137 + comment: comment, 141 138 }, 142 139 // specify the labeled post by strongRef 143 140 subject: { 144 141 $type: "com.atproto.admin.defs#repoRef", 145 - did, 142 + did: did, 146 143 }, 147 144 // put in the rest of the metadata 148 - createdBy: agent.did ?? "", 145 + createdBy: `${agent.did}`, 149 146 createdAt: new Date().toISOString(), 150 147 }, 151 148 { 152 149 encoding: "application/json", 153 150 headers: { 154 - "atproto-proxy": `${MOD_DID}#atproto_labeler`, 151 + "atproto-proxy": `${MOD_DID!}#atproto_labeler`, 155 152 "atproto-accept-labelers": 156 153 "did:plc:ar7c4by46qjdydhdevvrndac;redact", 157 154 }, 158 155 }, 159 156 ); 160 - } catch (error) { 161 - logger.error(`Error creating account comment for DID ${did}:`, error); 162 - throw error; 157 + } catch (e) { 158 + console.error(e); 163 159 } 164 160 }); 165 161 }; ··· 172 168 { 173 169 event: { 174 170 $type: "tools.ozone.moderation.defs#modEventReport", 175 - comment, 171 + comment: comment, 176 172 reportType: "com.atproto.moderation.defs#reasonOther", 177 173 }, 178 174 // specify the labeled post by strongRef 179 175 subject: { 180 176 $type: "com.atproto.admin.defs#repoRef", 181 - did, 177 + did: did, 182 178 }, 183 179 // put in the rest of the metadata 184 - createdBy: agent.did ?? "", 180 + createdBy: `${agent.did}`, 185 181 createdAt: new Date().toISOString(), 186 182 }, 187 183 { 188 184 encoding: "application/json", 189 185 headers: { 190 - "atproto-proxy": `${MOD_DID}#atproto_labeler`, 186 + "atproto-proxy": `${MOD_DID!}#atproto_labeler`, 191 187 "atproto-accept-labelers": 192 188 "did:plc:ar7c4by46qjdydhdevvrndac;redact", 193 189 }, 194 190 }, 195 191 ); 196 - } catch (error) { 197 - logger.error(`Error creating account report for DID ${did}:`, error); 198 - throw error; 192 + } catch (e) { 193 + console.error(e); 199 194 } 200 195 }); 201 196 }; ··· 212 207 } 213 208 logger.info(`New label added to list: ${newList.label}`); 214 209 215 - const listUri = `at://${MOD_DID}/app.bsky.graph.list/${newList.rkey}`; 210 + const listUri = `at://${MOD_DID!}/app.bsky.graph.list/${newList.rkey}`; 216 211 217 212 await limit(async () => { 218 213 try { 219 214 await agent.com.atproto.repo.createRecord({ 220 215 collection: "app.bsky.graph.listitem", 221 - repo: MOD_DID, 216 + repo: `${MOD_DID!}`, 222 217 record: { 223 218 subject: did, 224 219 list: listUri, 225 220 createdAt: new Date().toISOString(), 226 221 }, 227 222 }); 228 - } catch (error) { 229 - logger.error(`Error adding DID ${did} to list ${label}:`, error); 230 - throw error; 223 + } catch (e) { 224 + console.error(e); 231 225 } 232 226 }); 233 227 }; 234 228 235 - export function checkAccountLabels(_did: string) { 229 + export async function checkAccountLabels(did: string) { 236 230 /* try { 237 231 const repo = await limit(() => 238 232 agent.tools.ozone.moderation.getRepo(
src/processJetstream.ts

This is a binary file and will not be displayed.

+1 -1
src/utils.ts
··· 1 1 import logger from "./logger.js"; 2 2 3 - import { homoglyphMap } from "./homoglyphs"; 3 + import { homoglyphMap } from "./homoglyphs.js"; 4 4 5 5 /** 6 6 * Normalizes a string by converting it to lowercase, replacing homoglyphs,