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

Reverting

+2 -1
.claude/settings.local.json
··· 11 11 "mcp__git-mcp-server__git_push", 12 12 "Bash(bunx tsc:*)", 13 13 "Bash(DID=did:test OZONE_URL=http://test OZONE_PDS=http://test BSKY_HANDLE=test.bsky.social BSKY_PASSWORD=testpass bun --eval \"import(''./src/validateEnv.js'').then(m => m.validateEnvironment())\")", 14 - "mcp__git-mcp-server__git_add" 14 + "mcp__git-mcp-server__git_add", 15 + "Bash(bunx eslint:*)" 15 16 ], 16 17 "ask": [ 17 18 "curl"
+3 -4
src/agent.ts
··· 1 - import { AtpAgent } from '@atproto/api'; 2 - import { setGlobalDispatcher, Agent as Agent } from 'undici'; 3 - 1 + import { setGlobalDispatcher, Agent as Agent } from "undici"; 4 2 setGlobalDispatcher(new Agent({ connect: { timeout: 20_000 } })); 5 - import { BSKY_HANDLE, BSKY_PASSWORD, OZONE_PDS } from './config.js'; 3 + import { BSKY_HANDLE, BSKY_PASSWORD, OZONE_PDS } from "./config.js"; 4 + import { AtpAgent } from "@atproto/api"; 6 5 7 6 export const agent = new AtpAgent({ 8 7 service: `https://${OZONE_PDS}`,
+45 -50
src/checkHandles.ts
··· 1 - import { HANDLE_CHECKS } from './constants.js'; 2 - import logger from './logger.js'; 1 + import { HANDLE_CHECKS } from "./constants.js"; 2 + import logger from "./logger.js"; 3 3 import { 4 4 createAccountReport, 5 5 createAccountComment, 6 6 createAccountLabel, 7 - } from './moderation.js'; 7 + } from "./moderation.js"; 8 8 9 - export const checkHandle = ( 9 + export const checkHandle = async ( 10 10 did: string, 11 11 handle: string, 12 12 time: number, 13 13 ) => { 14 - try { 15 - // Get a list of labels 16 - const labels: string[] = Array.from( 17 - HANDLE_CHECKS, 18 - (handleCheck) => handleCheck.label, 14 + // Get a list of labels 15 + const labels: string[] = Array.from( 16 + HANDLE_CHECKS, 17 + (handleCheck) => handleCheck.label, 18 + ); 19 + 20 + // iterate through the labels 21 + labels.forEach((label) => { 22 + const checkList = HANDLE_CHECKS.find( 23 + (handleCheck) => handleCheck.label === label, 19 24 ); 20 25 21 - // iterate through the labels 22 - labels.forEach((label) => { 23 - const checkList = HANDLE_CHECKS.find( 24 - (handleCheck) => handleCheck.label === label, 25 - ); 26 + if (checkList?.ignoredDIDs) { 27 + if (checkList.ignoredDIDs.includes(did)) { 28 + logger.info(`Whitelisted DID: ${did}`); 29 + return; 30 + } 31 + } 26 32 27 - if (checkList?.ignoredDIDs) { 28 - if (checkList.ignoredDIDs.includes(did)) { 29 - logger.info(`Whitelisted DID: ${did}`); 33 + if (checkList!.check.test(handle)) { 34 + // False-positive checks 35 + if (checkList?.whitelist) { 36 + if (checkList?.whitelist.test(handle)) { 37 + logger.info(`Whitelisted phrase found for: ${handle}`); 30 38 return; 31 39 } 32 40 } 33 41 34 - if (checkList?.check.test(handle)) { 35 - // False-positive checks 36 - if (checkList.whitelist) { 37 - if (checkList.whitelist.test(handle)) { 38 - logger.info(`Whitelisted phrase found for: ${handle}`); 39 - return; 40 - } 42 + if (checkList?.toLabel === true) { 43 + logger.info(`[CHECKHANDLE]: Labeling ${did} for ${checkList!.label}`); 44 + { 45 + createAccountLabel( 46 + did, 47 + `${checkList!.label}`, 48 + `${time}: ${checkList!.comment} - ${handle}`, 49 + ); 41 50 } 51 + } 42 52 43 - if (checkList.toLabel) { 44 - logger.info(`[CHECKHANDLE]: Labeling ${did} for ${checkList.label}`); 45 - { 46 - void createAccountLabel( 47 - did, 48 - checkList.label, 49 - `${time.toString()}: ${checkList.comment} - ${handle}`, 50 - ); 51 - } 52 - } 53 + if (checkList?.reportAcct === true) { 54 + logger.info(`[CHECKHANDLE]: Reporting ${did} for ${checkList!.label}`); 55 + createAccountReport(did, `${time}: ${checkList!.comment} - ${handle}`); 56 + } 53 57 54 - if (checkList.reportAcct) { 55 - logger.info(`[CHECKHANDLE]: Reporting ${did} for ${checkList.label}`); 56 - void createAccountReport(did, `${time.toString()}: ${checkList.comment} - ${handle}`); 57 - } 58 - 59 - if (checkList.commentAcct) { 60 - logger.info( 61 - `[CHECKHANDLE]: Commenting on ${did} for ${checkList.label}`, 62 - ); 63 - void createAccountComment(did, `${time.toString()}: ${checkList.comment} - ${handle}`); 64 - } 58 + if (checkList?.commentAcct === true) { 59 + logger.info( 60 + `[CHECKHANDLE]: Commenting on ${did} for ${checkList!.label}`, 61 + ); 62 + createAccountComment(did, `${time}: ${checkList!.comment} - ${handle}`); 65 63 } 66 - }); 67 - } catch (error) { 68 - logger.error(`Error in checkHandle for ${did}:`, error); 69 - throw error; 70 - } 64 + } 65 + }); 71 66 };
+94 -99
src/checkPosts.ts
··· 1 - import { LINK_SHORTENER, POST_CHECKS } from './constants.js'; 2 - import logger from './logger.js'; 1 + import { LINK_SHORTENER, POST_CHECKS, langs } from "./constants.js"; 2 + import logger from "./logger.js"; 3 3 import { 4 4 createPostLabel, 5 5 createAccountReport, 6 6 createAccountComment, 7 7 createPostReport, 8 - } from './moderation.js'; 9 - import type { Post } from './types.js'; 10 - import { getFinalUrl, getLanguage } from './utils.js'; 8 + } from "./moderation.js"; 9 + import type { Post } from "./types.js"; 10 + import { getFinalUrl, getLanguage } from "./utils.js"; 11 11 12 12 export const checkPosts = async (post: Post[]) => { 13 - try { 14 - // Get a list of labels 15 - const labels: string[] = Array.from( 16 - POST_CHECKS, 17 - (postCheck) => postCheck.label, 18 - ); 13 + // Get a list of labels 14 + const labels: string[] = Array.from( 15 + POST_CHECKS, 16 + (postCheck) => postCheck.label, 17 + ); 19 18 20 - const urlRegex = /https?:\/\/[^\s]+/g; 19 + const urlRegex = /https?:\/\/[^\s]+/g; 21 20 22 - // Check for link shorteners 23 - if (LINK_SHORTENER.test(post[0].text)) { 24 - try { 25 - const url = post[0].text.match(urlRegex); 26 - if (url && LINK_SHORTENER.test(url[0])) { 27 - logger.info(`[CHECKPOSTS]: Checking shortened URL: ${url[0]}`); 28 - const finalUrl = await getFinalUrl(url[0]); 29 - if (finalUrl) { 30 - const originalUrl = post[0].text; 31 - post[0].text = post[0].text.replace(url[0], finalUrl); 32 - logger.info( 33 - `[CHECKPOSTS]: Shortened URL resolved: ${originalUrl} -> ${finalUrl}`, 34 - ); 35 - } 21 + // Check for link shorteners 22 + if (LINK_SHORTENER.test(post[0].text)) { 23 + try { 24 + const url = post[0].text.match(urlRegex); 25 + if (url && LINK_SHORTENER.test(url[0])) { 26 + logger.info(`[CHECKPOSTS]: Checking shortened URL: ${url[0]}`); 27 + const finalUrl = await getFinalUrl(url[0]); 28 + if (finalUrl) { 29 + const originalUrl = post[0].text; 30 + post[0].text = post[0].text.replace(url[0], finalUrl); 31 + logger.info( 32 + `[CHECKPOSTS]: Shortened URL resolved: ${originalUrl} -> ${finalUrl}`, 33 + ); 36 34 } 37 - } catch (error) { 38 - logger.error( 39 - `[CHECKPOSTS]: Failed to resolve shortened URL: ${post[0].text}`, 40 - error, 41 - ); 42 - // Keep the original URL if resolution fails 43 35 } 36 + } catch (error) { 37 + logger.error( 38 + `[CHECKPOSTS]: Failed to resolve shortened URL: ${post[0].text}`, 39 + error, 40 + ); 41 + // Keep the original URL if resolution fails 44 42 } 43 + } 45 44 46 - // Get the post's language 47 - const lang = await getLanguage(post[0].text); 45 + // Get the post's language 46 + const lang = await getLanguage(post[0].text); 48 47 49 - // iterate through the labels 50 - labels.forEach((label) => { 51 - const checkPost = POST_CHECKS.find( 52 - (postCheck) => postCheck.label === label, 53 - ); 48 + // iterate through the labels 49 + labels.forEach((label) => { 50 + const checkPost = POST_CHECKS.find( 51 + (postCheck) => postCheck.label === label, 52 + ); 54 53 55 - if (checkPost?.language || checkPost?.language !== undefined) { 56 - if (!checkPost.language.includes(lang)) { 57 - return; 58 - } 54 + if (checkPost?.language || checkPost?.language !== undefined) { 55 + if (!checkPost?.language.includes(lang)) { 56 + return; 59 57 } 58 + } 60 59 61 - if (checkPost?.ignoredDIDs) { 62 - if (checkPost.ignoredDIDs.includes(post[0].did)) { 63 - logger.info(`[CHECKPOSTS]: Whitelisted DID: ${post[0].did}`); 64 - return; 65 - } 60 + if (checkPost?.ignoredDIDs) { 61 + if (checkPost?.ignoredDIDs.includes(post[0].did)) { 62 + logger.info(`[CHECKPOSTS]: Whitelisted DID: ${post[0].did}`); 63 + return; 66 64 } 65 + } 67 66 68 - if (checkPost?.check.test(post[0].text)) { 67 + if (checkPost!.check.test(post[0].text)) { 69 68 // Check if post is whitelisted 70 - if (checkPost.whitelist) { 71 - if (checkPost.whitelist.test(post[0].text)) { 72 - logger.info('[CHECKPOSTS]: Whitelisted phrase found"'); 73 - return; 74 - } 69 + if (checkPost?.whitelist) { 70 + if (checkPost?.whitelist.test(post[0].text)) { 71 + logger.info(`[CHECKPOSTS]: Whitelisted phrase found"`); 72 + return; 75 73 } 74 + } 76 75 77 - if (checkPost.toLabel) { 78 - logger.info( 79 - `[CHECKPOSTS]: Labeling ${post[0].atURI} for ${checkPost.label}`, 80 - ); 81 - void createPostLabel( 82 - post[0].atURI, 83 - post[0].cid, 84 - checkPost.label, 85 - `${post[0].time.toString()}: ${checkPost.comment} at ${post[0].atURI} with text "${post[0].text}"`, 86 - ); 87 - } 76 + if (checkPost!.toLabel === true) { 77 + logger.info( 78 + `[CHECKPOSTS]: Labeling ${post[0].atURI} for ${checkPost!.label}`, 79 + ); 80 + createPostLabel( 81 + post[0].atURI, 82 + post[0].cid, 83 + `${checkPost!.label}`, 84 + `${post[0].time}: ${checkPost!.comment} at ${post[0].atURI} with text "${post[0].text}"`, 85 + ); 86 + } 88 87 89 - if (checkPost.reportPost) { 90 - logger.info( 91 - `[CHECKPOSTS]: Reporting ${post[0].atURI} for ${checkPost.label}`, 92 - ); 93 - logger.info(`Reporting: ${post[0].atURI}`); 94 - void createPostReport( 95 - post[0].atURI, 96 - post[0].cid, 97 - `${post[0].time.toString()}: ${checkPost.comment} at ${post[0].atURI} with text "${post[0].text}"`, 98 - ); 99 - } 88 + if (checkPost!.reportPost === true) { 89 + logger.info( 90 + `[CHECKPOSTS]: Reporting ${post[0].atURI} for ${checkPost!.label}`, 91 + ); 92 + logger.info(`Reporting: ${post[0].atURI}`); 93 + createPostReport( 94 + post[0].atURI, 95 + post[0].cid, 96 + `${post[0].time}: ${checkPost!.comment} at ${post[0].atURI} with text "${post[0].text}"`, 97 + ); 98 + } 100 99 101 - if (checkPost.reportAcct) { 102 - logger.info( 103 - `[CHECKPOSTS]: Reporting on ${post[0].did} for ${checkPost.label} in ${post[0].atURI}`, 104 - ); 105 - void createAccountReport( 106 - post[0].did, 107 - `${post[0].time.toString()}: ${checkPost.comment} at ${post[0].atURI} with text "${post[0].text}"`, 108 - ); 109 - } 100 + if (checkPost!.reportAcct === true) { 101 + logger.info( 102 + `[CHECKPOSTS]: Reporting on ${post[0].did} for ${checkPost!.label} in ${post[0].atURI}`, 103 + ); 104 + createAccountReport( 105 + post[0].did, 106 + `${post[0].time}: ${checkPost?.comment} at ${post[0].atURI} with text "${post[0].text}"`, 107 + ); 108 + } 110 109 111 - if (checkPost.commentAcct) { 112 - logger.info( 113 - `[CHECKPOSTS]: Commenting on ${post[0].did} for ${checkPost.label} in ${post[0].atURI}`, 114 - ); 115 - void createAccountComment( 116 - post[0].did, 117 - `${post[0].time.toString()}: ${checkPost.comment} at ${post[0].atURI} with text "${post[0].text}"`, 118 - ); 119 - } 110 + if (checkPost!.commentAcct === true) { 111 + logger.info( 112 + `[CHECKPOSTS]: Commenting on ${post[0].did} for ${checkPost!.label} in ${post[0].atURI}`, 113 + ); 114 + createAccountComment( 115 + post[0].did, 116 + `${post[0].time}: ${checkPost?.comment} at ${post[0].atURI} with text "${post[0].text}"`, 117 + ); 120 118 } 121 - }); 122 - } catch (error) { 123 - logger.error(`Error in checkPosts for ${post[0]?.did}:`, error); 124 - throw error; 125 - } 119 + } 120 + }); 126 121 };
+8 -8
src/checkProfiles.ts
··· 1 - import { PROFILE_CHECKS } from './constants.js'; 2 - import logger from './logger.js'; 1 + import { PROFILE_CHECKS } from "./constants.js"; 2 + import logger from "./logger.js"; 3 3 import { 4 4 createAccountReport, 5 5 createAccountLabel, 6 6 createAccountComment, 7 - } from './moderation.js'; 8 - import { getLanguage } from './utils.js'; 7 + } from "./moderation.js"; 8 + import { getLanguage } from "./utils.js"; 9 9 10 10 export const checkDescription = async ( 11 11 did: string, ··· 44 44 if (description) { 45 45 if (checkProfiles?.description === true) { 46 46 if (checkProfiles.check.test(description)) { 47 - // Check if description is whitelisted 47 + // Check if description is whitelisted 48 48 if (checkProfiles.whitelist) { 49 49 if (checkProfiles.whitelist.test(description)) { 50 - logger.info('[CHECKDESCRIPTION]: Whitelisted phrase found.'); 50 + logger.info("[CHECKDESCRIPTION]: Whitelisted phrase found."); 51 51 return; 52 52 } 53 53 } ··· 130 130 if (displayName) { 131 131 if (checkProfiles?.displayName === true) { 132 132 if (checkProfiles.check.test(displayName)) { 133 - // Check if displayName is whitelisted 133 + // Check if displayName is whitelisted 134 134 if (checkProfiles.whitelist) { 135 135 if (checkProfiles.whitelist.test(displayName)) { 136 - logger.info('[CHECKDISPLAYNAME]: Whitelisted phrase found.'); 136 + logger.info("[CHECKDISPLAYNAME]: Whitelisted phrase found."); 137 137 return; 138 138 } 139 139 }
+74 -81
src/checkStarterPack.ts
··· 1 - import { PROFILE_CHECKS, STARTERPACK_CHECKS } from './constants.js'; 2 - import logger from './logger.js'; 1 + import { PROFILE_CHECKS, STARTERPACK_CHECKS } from "./constants.js"; 2 + import logger from "./logger.js"; 3 3 import { 4 4 createAccountLabel, 5 5 createAccountReport, 6 6 createPostLabel, 7 - } from './moderation.js'; 7 + } from "./moderation.js"; 8 8 9 - export const checkStarterPack = ( 9 + export const checkStarterPack = async ( 10 10 did: string, 11 11 time: number, 12 12 atURI: string, 13 13 ) => { 14 - try { 15 - // Get a list of labels 16 - const labels: string[] = Array.from( 17 - PROFILE_CHECKS, 18 - (profileCheck) => profileCheck.label, 14 + // Get a list of labels 15 + const labels: string[] = Array.from( 16 + PROFILE_CHECKS, 17 + (profileCheck) => profileCheck.label, 18 + ); 19 + 20 + // iterate through the labels 21 + labels.forEach((label) => { 22 + const checkProfiles = PROFILE_CHECKS.find( 23 + (profileCheck) => profileCheck.label === label, 19 24 ); 20 25 21 - // iterate through the labels 22 - labels.forEach((label) => { 23 - const checkProfiles = PROFILE_CHECKS.find( 24 - (profileCheck) => profileCheck.label === label, 25 - ); 26 - 27 - // Check if DID is whitelisted 28 - if (checkProfiles?.ignoredDIDs) { 29 - if (checkProfiles.ignoredDIDs.includes(did)) { 30 - logger.info(`Whitelisted DID: ${did}`); return; 31 - } 26 + // Check if DID is whitelisted 27 + if (checkProfiles?.ignoredDIDs) { 28 + if (checkProfiles.ignoredDIDs.includes(did)) { 29 + return logger.info(`Whitelisted DID: ${did}`); 32 30 } 31 + } 33 32 34 - if (atURI) { 35 - if (checkProfiles?.starterPacks) { 36 - if (checkProfiles.starterPacks.includes(atURI)) { 37 - logger.info(`Account joined via starter pack at: ${atURI}`); 38 - void createAccountLabel( 39 - did, 40 - checkProfiles.label, 41 - `${time.toString()}: ${checkProfiles.comment} - Account joined via starter pack at: ${atURI}`, 42 - ); 43 - } 33 + if (atURI) { 34 + if (checkProfiles?.starterPacks) { 35 + if (checkProfiles?.starterPacks.includes(atURI)) { 36 + logger.info(`Account joined via starter pack at: ${atURI}`); 37 + createAccountLabel( 38 + did, 39 + `${checkProfiles!.label}`, 40 + `${time}: ${checkProfiles!.comment} - Account joined via starter pack at: ${atURI}`, 41 + ); 44 42 } 45 43 } 46 - }); 47 - } catch (error) { 48 - logger.error(`Error in checkStarterPack for ${did}:`, error); 49 - throw error; 50 - } 44 + } 45 + }); 51 46 }; 52 47 53 - export const checkNewStarterPack = ( 48 + export const checkNewStarterPack = async ( 54 49 did: string, 55 50 time: number, 56 51 atURI: string, ··· 58 53 packName: string | undefined, 59 54 description: string | undefined, 60 55 ) => { 61 - try { 62 - const labels: string[] = STARTERPACK_CHECKS.map((SPCheck) => SPCheck.label); 56 + const labels: string[] = Array.from( 57 + STARTERPACK_CHECKS, 58 + (SPCheck) => SPCheck.label, 59 + ); 60 + 61 + labels.forEach((label) => { 62 + const checkList = PROFILE_CHECKS.find((SPCheck) => SPCheck.label === label); 63 63 64 - labels.forEach((label) => { 65 - const checkList = STARTERPACK_CHECKS.find((SPCheck) => SPCheck.label === label); 64 + if (checkList?.knownVectors?.includes(did)) { 65 + createPostLabel( 66 + atURI, 67 + cid, 68 + `${checkList!.label}`, 69 + `${time}: Starter pack created by known vector for ${checkList!.label} at: ${atURI}"`, 70 + ); 71 + createAccountReport( 72 + did, 73 + `${time}: Starter pack created by known vector for ${checkList!.label} at: ${atURI}"`, 74 + ); 75 + } 66 76 67 - if (checkList?.knownVectors?.includes(did)) { 68 - void createPostLabel( 77 + if (description) { 78 + if (checkList!.check.test(description)) { 79 + logger.info(`Labeling post: ${atURI}`); 80 + createPostLabel( 69 81 atURI, 70 82 cid, 71 - checkList.label, 72 - `${time.toString()}: Starter pack created by known vector for ${checkList.label} at: ${atURI}"`, 83 + `${checkList!.label}`, 84 + `${time}: ${checkList!.comment} at ${atURI} with text "${description}"`, 73 85 ); 74 - void createAccountReport( 86 + createAccountReport( 75 87 did, 76 - `${time.toString()}: Starter pack created by known vector for ${checkList.label} at: ${atURI}"`, 88 + `${time}: ${checkList!.comment} at ${atURI} with text "${description}"`, 77 89 ); 78 90 } 91 + } 79 92 80 - if (description) { 81 - if (checkList?.check.test(description)) { 82 - logger.info(`Labeling post: ${atURI}`); 83 - void createPostLabel( 84 - atURI, 85 - cid, 86 - checkList.label, 87 - `${time.toString()}: ${checkList.comment} at ${atURI} with text "${description}"`, 88 - ); 89 - void createAccountReport( 90 - did, 91 - `${time.toString()}: ${checkList.comment} at ${atURI} with text "${description}"`, 92 - ); 93 - } 94 - } 95 - 96 - if (packName) { 97 - if (checkList?.check.test(packName)) { 98 - logger.info(`Labeling post: ${atURI}`); 99 - void createPostLabel( 100 - atURI, 101 - cid, 102 - checkList.label, 103 - `${time.toString()}: ${checkList.comment} at ${atURI} with pack name "${packName}"`, 104 - ); 105 - void createAccountReport( 106 - did, 107 - `${time.toString()}: ${checkList.comment} at ${atURI} with pack name "${packName}"`, 108 - ); 109 - } 93 + if (packName) { 94 + if (checkList!.check.test(packName)) { 95 + logger.info(`Labeling post: ${atURI}`); 96 + createPostLabel( 97 + atURI, 98 + cid, 99 + `${checkList!.label}`, 100 + `${time}: ${checkList!.comment} at ${atURI} with pack name "${packName}"`, 101 + ); 102 + createAccountReport( 103 + did, 104 + `${time}: ${checkList!.comment} at ${atURI} with pack name "${packName}"`, 105 + ); 110 106 } 111 - }); 112 - } catch (error) { 113 - logger.error(`Error in checkNewStarterPack for ${did}:`, error); 114 - throw error; 115 - } 107 + } 108 + }); 116 109 };
+13 -13
src/config.ts
··· 1 - import 'dotenv/config'; 1 + import "dotenv/config"; 2 2 3 - export const MOD_DID = process.env.DID ?? ''; 4 - export const OZONE_URL = process.env.OZONE_URL ?? ''; 5 - export const OZONE_PDS = process.env.OZONE_PDS ?? ''; 6 - export const BSKY_HANDLE = process.env.BSKY_HANDLE ?? ''; 7 - export const BSKY_PASSWORD = process.env.BSKY_PASSWORD ?? ''; 8 - export const HOST = process.env.HOST ?? '127.0.0.1'; 3 + export const MOD_DID = process.env.DID ?? ""; 4 + export const OZONE_URL = process.env.OZONE_URL ?? ""; 5 + export const OZONE_PDS = process.env.OZONE_PDS ?? ""; 6 + export const BSKY_HANDLE = process.env.BSKY_HANDLE ?? ""; 7 + export const BSKY_PASSWORD = process.env.BSKY_PASSWORD ?? ""; 8 + export const HOST = process.env.HOST ?? "127.0.0.1"; 9 9 export const PORT = process.env.PORT ? Number(process.env.PORT) : 4100; 10 10 export const METRICS_PORT = process.env.METRICS_PORT 11 11 ? Number(process.env.METRICS_PORT) 12 12 : 4101; // Left this intact from the code I adapted this from 13 13 export const FIREHOSE_URL = 14 - process.env.FIREHOSE_URL ?? 'wss://jetstream1.us-east.bsky.network/subscribe'; 14 + process.env.FIREHOSE_URL ?? "wss://jetstream.atproto.tools/subscribe"; 15 15 export const WANTED_COLLECTION = [ 16 - 'app.bsky.feed.post', 17 - 'app.bsky.actor.defs', 18 - 'app.bsky.actor.profile', 16 + "app.bsky.feed.post", 17 + "app.bsky.actor.defs", 18 + "app.bsky.actor.profile", 19 19 ]; 20 20 export const CURSOR_UPDATE_INTERVAL = process.env.CURSOR_UPDATE_INTERVAL 21 21 ? Number(process.env.CURSOR_UPDATE_INTERVAL) 22 22 : 60000; 23 - export const { LABEL_LIMIT } = process.env; 24 - export const { LABEL_LIMIT_WAIT } = process.env; 23 + export const LABEL_LIMIT = process.env.LABEL_LIMIT; 24 + export const LABEL_LIMIT_WAIT = process.env.LABEL_LIMIT_WAIT;
+308
src/homoglyphs.ts
··· 1 + /* eslint-disable no-misleading-character-class */ 2 + 3 + export const homoglyphMap: Record<string, string> = { 4 + // Confusables for 'a' 5 + á: "a", 6 + à: "a", 7 + ă: "a", 8 + ắ: "a", 9 + ằ: "a", 10 + ẵ: "a", 11 + ẳ: "a", 12 + â: "a", 13 + ấ: "a", 14 + ầ: "a", 15 + ẫ: "a", 16 + ẩ: "a", 17 + ǎ: "a", 18 + å: "a", 19 + ǻ: "a", 20 + ä: "a", 21 + ǟ: "a", 22 + ã: "a", 23 + ȧ: "a", 24 + ǡ: "a", 25 + ą: "a", 26 + ą́: "a", 27 + ą̃: "a", 28 + ā: "a", 29 + ā̀: "a", 30 + ả: "a", 31 + ȁ: "a", 32 + a̋: "a", 33 + ȃ: "a", 34 + ạ: "a", 35 + ặ: "a", 36 + ậ: "a", 37 + ḁ: "a", 38 + ⱥ: "a", 39 + "ꞻ": "a", 40 + ᶏ: "a", 41 + ẚ: "a", 42 + a: "a", 43 + "@": "a", 44 + "4": "a", 45 + 46 + // Confusables for 'e' 47 + "3": "e", 48 + є: "e", 49 + е: "e", 50 + é: "e", 51 + è: "e", 52 + ĕ: "e", 53 + ê: "e", 54 + ế: "e", 55 + ề: "e", 56 + ễ: "e", 57 + ể: "e", 58 + ê̄: "e", 59 + ê̌: "e", 60 + ě: "e", 61 + ë: "e", 62 + ẽ: "e", 63 + ė: "e", 64 + ė́: "e", 65 + ė̃: "e", 66 + ȩ: "e", 67 + ḝ: "e", 68 + ę: "e", 69 + ę́: "e", 70 + ę̃: "e", 71 + ē: "e", 72 + ḗ: "e", 73 + ḕ: "e", 74 + ẻ: "e", 75 + ȅ: "e", 76 + e̋: "e", 77 + ȇ: "e", 78 + ẹ: "e", 79 + ệ: "e", 80 + ḙ: "e", 81 + ḛ: "e", 82 + ɇ: "e", 83 + e̩: "e", 84 + è̩: "e", 85 + é̩: "e", 86 + ᶒ: "e", 87 + ⱸ: "e", 88 + ꬴ: "e", 89 + ꬳ: "e", 90 + e: "e", 91 + 92 + // Confusables for 'g' 93 + ǵ: "g", 94 + ğ: "g", 95 + ĝ: "g", 96 + ǧ: "g", 97 + ġ: "g", 98 + g̃: "g", 99 + ģ: "g", 100 + ḡ: "g", 101 + ǥ: "g", 102 + ꞡ: "g", 103 + ɠ: "g", 104 + ᶃ: "g", 105 + ꬶ: "g", 106 + g: "g", 107 + q: "g", 108 + ꝗ: "g", 109 + ꝙ: "g", 110 + ɋ: "g", 111 + ʠ: "g", 112 + 113 + // Confusables for 'i' and 'l' 114 + í: "i", 115 + i̇́: "i", 116 + ì: "i", 117 + i̇̀: "i", 118 + ĭ: "i", 119 + î: "i", 120 + ǐ: "i", 121 + ï: "i", 122 + ḯ: "i", 123 + ĩ: "i", 124 + i̇̃: "i", 125 + į: "i", 126 + į̇́: "i", 127 + į̇̃: "i", 128 + ī: "i", 129 + ī̀: "i", 130 + ỉ: "i", 131 + ȉ: "i", 132 + i̋: "i", 133 + ȋ: "i", 134 + ị: "i", 135 + "ꞽ": "i", 136 + ḭ: "i", 137 + ɨ: "i", 138 + ᶖ: "i", 139 + ı: "i", 140 + i: "i", 141 + "1": "i", 142 + ĺ: "l", 143 + ľ: "l", 144 + ļ: "l", 145 + ḷ: "l", 146 + ḹ: "l", 147 + l̃: "l", 148 + ḽ: "l", 149 + ḻ: "l", 150 + ł: "l", 151 + ŀ: "l", 152 + ƚ: "l", 153 + ꝉ: "l", 154 + ⱡ: "l", 155 + ɫ: "l", 156 + ɬ: "l", 157 + ꞎ: "l", 158 + ꬷ: "l", 159 + ꬸ: "l", 160 + ꬹ: "l", 161 + ᶅ: "l", 162 + ɭ: "l", 163 + ȴ: "l", 164 + l: "l", 165 + 166 + // Confusables for 'k' 167 + ḱ: "k", 168 + ǩ: "k", 169 + ķ: "k", 170 + ḳ: "k", 171 + ḵ: "k", 172 + ƙ: "k", 173 + ⱪ: "k", 174 + ᶄ: "k", 175 + ꝁ: "k", 176 + ꝃ: "k", 177 + ꝅ: "k", 178 + ꞣ: "k", 179 + 180 + // Confusables for 'n' 181 + ń: "n", 182 + ň: "n", 183 + ñ: "n", 184 + ṅ: "n", 185 + ņ: "n", 186 + ṇ: "n", 187 + ṋ: "n", 188 + ǹ: "n", 189 + Ƞ: "n", 190 + ƞ: "n", 191 + ᵰ: "n", 192 + ᶇ: "n", 193 + ɳ: "n", 194 + ȵ: "n", 195 + ⁿ: "n", 196 + n: "n", 197 + מ: "n", 198 + и: "n", 199 + п: "n", 200 + ꬻ: "n", 201 + ꬼ: "n", 202 + n̈: "n", 203 + ɲ: "n", 204 + ŋ: "n", 205 + ꞑ: "n", 206 + ꞥ: "n", 207 + 208 + // Confusables for 'o' 209 + ó: "o", 210 + ò: "o", 211 + ŏ: "o", 212 + ô: "o", 213 + ố: "o", 214 + ồ: "o", 215 + ỗ: "o", 216 + ổ: "o", 217 + ǒ: "o", 218 + ö: "o", 219 + ȫ: "o", 220 + ő: "o", 221 + õ: "o", 222 + ṍ: "o", 223 + ṏ: "o", 224 + ȭ: "o", 225 + ȯ: "o", 226 + o͘: "o", 227 + ȱ: "o", 228 + ø: "o", 229 + ǿ: "o", 230 + ǫ: "o", 231 + ǭ: "o", 232 + ō: "o", 233 + ṓ: "o", 234 + ṑ: "o", 235 + ỏ: "o", 236 + ȍ: "o", 237 + ȏ: "o", 238 + ơ: "o", 239 + ớ: "o", 240 + ờ: "o", 241 + ỡ: "o", 242 + ở: "o", 243 + ợ: "o", 244 + ọ: "o", 245 + ộ: "o", 246 + o̩: "o", 247 + ò̩: "o", 248 + ó̩: "o", 249 + ɵ: "o", 250 + ꝋ: "o", 251 + ꝍ: "o", 252 + ⱺ: "o", 253 + o: "o", 254 + "0": "o", 255 + 256 + // Confusables for 'r' 257 + ŕ: "r", 258 + ř: "r", 259 + ṙ: "r", 260 + ŗ: "r", 261 + ȑ: "r", 262 + ȓ: "r", 263 + ṛ: "r", 264 + ṝ: "r", 265 + ṟ: "r", 266 + r̃: "r", 267 + ɍ: "r", 268 + ꞧ: "r", 269 + ɽ: "r", 270 + ᵲ: "r", 271 + ᶉ: "r", 272 + ꭉ: "r", 273 + 274 + // Confusables for 's' 275 + ś: "s", 276 + ṥ: "s", 277 + ŝ: "s", 278 + š: "s", 279 + ṧ: "s", 280 + ṡ: "s", 281 + ş: "s", 282 + ṣ: "s", 283 + ṩ: "s", 284 + ș: "s", 285 + s̩: "s", 286 + ꞩ: "s", 287 + ȿ: "s", 288 + ʂ: "s", 289 + ᶊ: "s", 290 + ᵴ: "s", 291 + 292 + // Confusables for 't' 293 + ť: "t", 294 + ṫ: "t", 295 + ţ: "t", 296 + ṭ: "t", 297 + ț: "t", 298 + ṱ: "t", 299 + ṯ: "t", 300 + ŧ: "t", 301 + ⱦ: "t", 302 + ƭ: "t", 303 + ʈ: "t", 304 + ẗ: "t", 305 + ᵵ: "t", 306 + ƫ: "t", 307 + ȶ: "t", 308 + };
+1 -1
src/limits.ts
··· 1 - import { pRateLimit } from 'p-ratelimit'; // TypeScript 1 + import { pRateLimit } from "p-ratelimit"; // TypeScript 2 2 3 3 // create a rate limiter that allows up to 30 API calls per second, 4 4 // with max concurrency of 10
+21 -21
src/lists.ts
··· 1 - import type { List } from './types.js'; 1 + import { List } from "./types.js"; 2 2 3 3 export const LISTS: List[] = [ 4 4 { 5 - label: 'blue-heart-emoji', 6 - rkey: '3lfbtgosyyi22', 5 + label: "blue-heart-emoji", 6 + rkey: "3lfbtgosyyi22", 7 7 }, 8 8 { 9 - label: 'troll', 10 - rkey: '3lbckxhgu3r2v', 9 + label: "troll", 10 + rkey: "3lbckxhgu3r2v", 11 11 }, 12 12 { 13 - label: 'maga-trump', 14 - rkey: '3l53cjwlt4o2s', 13 + label: "maga-trump", 14 + rkey: "3l53cjwlt4o2s", 15 15 }, 16 16 { 17 - label: 'elon-musk', 18 - rkey: '3l72tte74wa2m', 17 + label: "elon-musk", 18 + rkey: "3l72tte74wa2m", 19 19 }, 20 20 { 21 - label: 'rmve-imve', 22 - rkey: '3l6tfurf7li27', 21 + label: "rmve-imve", 22 + rkey: "3l6tfurf7li27", 23 23 }, 24 24 { 25 - label: 'nazi-symbolism', 26 - rkey: '3l6vdudxgeb2z', 25 + label: "nazi-symbolism", 26 + rkey: "3l6vdudxgeb2z", 27 27 }, 28 28 { 29 - label: 'hammer-sickle', 30 - rkey: '3l4ue6w2aur2v', 29 + label: "hammer-sickle", 30 + rkey: "3l4ue6w2aur2v", 31 31 }, 32 32 { 33 - label: 'inverted-red-triangle', 34 - rkey: '3l4ueabtpec2a', 33 + label: "inverted-red-triangle", 34 + rkey: "3l4ueabtpec2a", 35 35 }, 36 36 { 37 - label: 'automated-reply-guy', 38 - rkey: '3lch7qbvzpx23', 37 + label: "automated-reply-guy", 38 + rkey: "3lch7qbvzpx23", 39 39 }, 40 40 { 41 - label: 'terf-gc', 42 - rkey: '3lcqjqjdejs2x', 41 + label: "terf-gc", 42 + rkey: "3lcqjqjdejs2x", 43 43 }, 44 44 ];
+10 -10
src/logger.ts
··· 1 - import { pino } from 'pino'; 1 + import { pino } from "pino"; 2 2 3 3 const logger = pino({ 4 - level: process.env.LOG_LEVEL ?? 'info', 4 + level: process.env.LOG_LEVEL ?? "info", 5 5 transport: 6 - process.env.NODE_ENV !== 'production' 6 + process.env.NODE_ENV !== "production" 7 7 ? { 8 - target: 'pino-pretty', 9 - options: { 10 - colorize: true, 11 - translateTime: 'SYS:standard', 12 - ignore: 'pid,hostname', 13 - }, 14 - } 8 + target: "pino-pretty", 9 + options: { 10 + colorize: true, 11 + translateTime: "SYS:standard", 12 + ignore: "pid,hostname", 13 + }, 14 + } 15 15 : undefined, 16 16 timestamp: pino.stdTimeFunctions.isoTime, 17 17 });
+1 -1
src/main.ts
··· 17 17 METRICS_PORT, 18 18 WANTED_COLLECTION, 19 19 } from "./config.js"; 20 - import { validateEnvironment } from "./validateEnv.js"; 21 20 import logger from "./logger.js"; 22 21 import { startMetricsServer } from "./metrics.js"; 22 + import { validateEnvironment } from "./validateEnv.js"; 23 23 import type { Post, LinkFeature } from "./types.js"; 24 24 25 25 validateEnvironment();
+6 -6
src/metrics.ts
··· 1 - import express from 'express'; 2 - import { Registry, collectDefaultMetrics } from 'prom-client'; 1 + import express from "express"; 2 + import { Registry, collectDefaultMetrics } from "prom-client"; 3 3 4 - import logger from './logger.js'; 4 + import logger from "./logger.js"; 5 5 6 6 const register = new Registry(); 7 7 collectDefaultMetrics({ register }); 8 8 9 9 const app = express(); 10 10 11 - app.get('/metrics', (req, res) => { 11 + app.get("/metrics", (req, res) => { 12 12 register 13 13 .metrics() 14 14 .then((metrics) => { 15 - res.set('Content-Type', register.contentType); 15 + res.set("Content-Type", register.contentType); 16 16 res.send(metrics); 17 17 }) 18 18 .catch((ex: unknown) => { ··· 21 21 }); 22 22 }); 23 23 24 - export const startMetricsServer = (port: number, host = '127.0.0.1') => { 24 + export const startMetricsServer = (port: number, host = "127.0.0.1") => { 25 25 return app.listen(port, host, () => { 26 26 logger.info(`Metrics server is listening on ${host}:${port}`); 27 27 });
+43 -43
src/moderation.ts
··· 1 - import { agent, isLoggedIn } from './agent.js'; 2 - import { MOD_DID } from './config.js'; 3 - import { limit } from './limits.js'; 4 - import { LISTS } from './lists.js'; 5 - import logger from './logger.js'; 1 + import { agent, isLoggedIn } from "./agent.js"; 2 + import { MOD_DID } from "./config.js"; 3 + import { limit } from "./limits.js"; 4 + import { LISTS } from "./lists.js"; 5 + import logger from "./logger.js"; 6 6 7 7 export const createPostLabel = async ( 8 8 uri: string, ··· 16 16 return await agent.tools.ozone.moderation.emitEvent( 17 17 { 18 18 event: { 19 - $type: 'tools.ozone.moderation.defs#modEventLabel', 19 + $type: "tools.ozone.moderation.defs#modEventLabel", 20 20 comment, 21 21 createLabelVals: [label], 22 22 negateLabelVals: [], 23 23 }, 24 24 // specify the labeled post by strongRef 25 25 subject: { 26 - $type: 'com.atproto.repo.strongRef', 26 + $type: "com.atproto.repo.strongRef", 27 27 uri, 28 28 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 - encoding: 'application/json', 35 + encoding: "application/json", 36 36 headers: { 37 - 'atproto-proxy': `${MOD_DID}#atproto_labeler`, 38 - 'atproto-accept-labelers': 39 - 'did:plc:ar7c4by46qjdydhdevvrndac;redact', 37 + "atproto-proxy": `${MOD_DID}#atproto_labeler`, 38 + "atproto-accept-labelers": 39 + "did:plc:ar7c4by46qjdydhdevvrndac;redact", 40 40 }, 41 41 }, 42 42 ); ··· 58 58 await agent.tools.ozone.moderation.emitEvent( 59 59 { 60 60 event: { 61 - $type: 'tools.ozone.moderation.defs#modEventLabel', 61 + $type: "tools.ozone.moderation.defs#modEventLabel", 62 62 comment, 63 63 createLabelVals: [label], 64 64 negateLabelVals: [], 65 65 }, 66 66 // specify the labeled post by strongRef 67 67 subject: { 68 - $type: 'com.atproto.admin.defs#repoRef', 68 + $type: "com.atproto.admin.defs#repoRef", 69 69 did, 70 70 }, 71 71 // put in the rest of the metadata 72 - createdBy: agent.did ?? '', 72 + createdBy: agent.did ?? "", 73 73 createdAt: new Date().toISOString(), 74 74 }, 75 75 { 76 - encoding: 'application/json', 76 + encoding: "application/json", 77 77 headers: { 78 - 'atproto-proxy': `${MOD_DID}#atproto_labeler`, 79 - 'atproto-accept-labelers': 80 - 'did:plc:ar7c4by46qjdydhdevvrndac;redact', 78 + "atproto-proxy": `${MOD_DID}#atproto_labeler`, 79 + "atproto-accept-labelers": 80 + "did:plc:ar7c4by46qjdydhdevvrndac;redact", 81 81 }, 82 82 }, 83 83 ); ··· 99 99 return await agent.tools.ozone.moderation.emitEvent( 100 100 { 101 101 event: { 102 - $type: 'tools.ozone.moderation.defs#modEventReport', 102 + $type: "tools.ozone.moderation.defs#modEventReport", 103 103 comment, 104 - reportType: 'com.atproto.moderation.defs#reasonOther', 104 + reportType: "com.atproto.moderation.defs#reasonOther", 105 105 }, 106 106 // specify the labeled post by strongRef 107 107 subject: { 108 - $type: 'com.atproto.repo.strongRef', 108 + $type: "com.atproto.repo.strongRef", 109 109 uri, 110 110 cid, 111 111 }, 112 112 // put in the rest of the metadata 113 - createdBy: agent.did ?? '', 113 + createdBy: agent.did ?? "", 114 114 createdAt: new Date().toISOString(), 115 115 }, 116 116 { 117 - encoding: 'application/json', 117 + encoding: "application/json", 118 118 headers: { 119 - 'atproto-proxy': `${MOD_DID}#atproto_labeler`, 120 - 'atproto-accept-labelers': 121 - 'did:plc:ar7c4by46qjdydhdevvrndac;redact', 119 + "atproto-proxy": `${MOD_DID}#atproto_labeler`, 120 + "atproto-accept-labelers": 121 + "did:plc:ar7c4by46qjdydhdevvrndac;redact", 122 122 }, 123 123 }, 124 124 ); ··· 136 136 await agent.tools.ozone.moderation.emitEvent( 137 137 { 138 138 event: { 139 - $type: 'tools.ozone.moderation.defs#modEventComment', 139 + $type: "tools.ozone.moderation.defs#modEventComment", 140 140 comment, 141 141 }, 142 142 // specify the labeled post by strongRef 143 143 subject: { 144 - $type: 'com.atproto.admin.defs#repoRef', 144 + $type: "com.atproto.admin.defs#repoRef", 145 145 did, 146 146 }, 147 147 // put in the rest of the metadata 148 - createdBy: agent.did ?? '', 148 + createdBy: agent.did ?? "", 149 149 createdAt: new Date().toISOString(), 150 150 }, 151 151 { 152 - encoding: 'application/json', 152 + encoding: "application/json", 153 153 headers: { 154 - 'atproto-proxy': `${MOD_DID}#atproto_labeler`, 155 - 'atproto-accept-labelers': 156 - 'did:plc:ar7c4by46qjdydhdevvrndac;redact', 154 + "atproto-proxy": `${MOD_DID}#atproto_labeler`, 155 + "atproto-accept-labelers": 156 + "did:plc:ar7c4by46qjdydhdevvrndac;redact", 157 157 }, 158 158 }, 159 159 ); ··· 171 171 await agent.tools.ozone.moderation.emitEvent( 172 172 { 173 173 event: { 174 - $type: 'tools.ozone.moderation.defs#modEventReport', 174 + $type: "tools.ozone.moderation.defs#modEventReport", 175 175 comment, 176 - reportType: 'com.atproto.moderation.defs#reasonOther', 176 + reportType: "com.atproto.moderation.defs#reasonOther", 177 177 }, 178 178 // specify the labeled post by strongRef 179 179 subject: { 180 - $type: 'com.atproto.admin.defs#repoRef', 180 + $type: "com.atproto.admin.defs#repoRef", 181 181 did, 182 182 }, 183 183 // put in the rest of the metadata 184 - createdBy: agent.did ?? '', 184 + createdBy: agent.did ?? "", 185 185 createdAt: new Date().toISOString(), 186 186 }, 187 187 { 188 - encoding: 'application/json', 188 + encoding: "application/json", 189 189 headers: { 190 - 'atproto-proxy': `${MOD_DID}#atproto_labeler`, 191 - 'atproto-accept-labelers': 192 - 'did:plc:ar7c4by46qjdydhdevvrndac;redact', 190 + "atproto-proxy": `${MOD_DID}#atproto_labeler`, 191 + "atproto-accept-labelers": 192 + "did:plc:ar7c4by46qjdydhdevvrndac;redact", 193 193 }, 194 194 }, 195 195 ); ··· 217 217 await limit(async () => { 218 218 try { 219 219 await agent.com.atproto.repo.createRecord({ 220 - collection: 'app.bsky.graph.listitem', 220 + collection: "app.bsky.graph.listitem", 221 221 repo: MOD_DID, 222 222 record: { 223 223 subject: did,
+24 -25
src/monitor.ts
··· 1 - import { describe } from 'node:test'; 2 - 3 - import { PROFILE_CHECKS } from './constants.js'; 4 - import logger from './logger.js'; 5 - import { createAccountReport, createAccountLabel } from './moderation.js'; 1 + import { describe } from "node:test"; 2 + import { PROFILE_CHECKS } from "./constants.js"; 3 + import logger from "./logger.js"; 4 + import { createAccountReport, createAccountLabel } from "./moderation.js"; 6 5 7 6 export const monitorDescription = async ( 8 7 did: string, ··· 25 24 // Check if DID is whitelisted 26 25 if (checkProfiles?.ignoredDIDs) { 27 26 if (checkProfiles.ignoredDIDs.includes(did)) { 28 - logger.info(`Whitelisted DID: ${did}`); return; 27 + return logger.info(`Whitelisted DID: ${did}`); 29 28 } 30 29 } 31 30 32 31 if (description) { 33 32 if (checkProfiles?.description === true) { 34 - if (checkProfiles.check.test(description)) { 35 - if (checkProfiles.whitelist) { 36 - if (checkProfiles.whitelist.test(description)) { 37 - logger.info('Whitelisted phrase found.'); 33 + if (checkProfiles!.check.test(description)) { 34 + if (checkProfiles!.whitelist) { 35 + if (checkProfiles!.whitelist.test(description)) { 36 + logger.info(`Whitelisted phrase found.`); 38 37 return; 39 38 } 40 39 } else { 41 - logger.info(`${checkProfiles.label} in description for ${did}`); 40 + logger.info(`${checkProfiles!.label} in description for ${did}`); 42 41 } 43 42 44 - if (checkProfiles.reportOnly === true) { 43 + if (checkProfiles!.reportOnly === true) { 45 44 createAccountReport( 46 45 did, 47 - `${time}: ${checkProfiles.comment} - ${displayName} - ${description}`, 46 + `${time}: ${checkProfiles!.comment} - ${displayName} - ${description}`, 48 47 ); 49 48 return; 50 49 } else { 51 50 createAccountLabel( 52 51 did, 53 - checkProfiles.label, 54 - `${time}: ${checkProfiles.comment}`, 52 + `${checkProfiles!.label}`, 53 + `${time}: ${checkProfiles!.comment}`, 55 54 ); 56 55 } 57 56 } ··· 81 80 // Check if DID is whitelisted 82 81 if (checkProfiles?.ignoredDIDs) { 83 82 if (checkProfiles.ignoredDIDs.includes(did)) { 84 - logger.info(`Whitelisted DID: ${did}`); return; 83 + return logger.info(`Whitelisted DID: ${did}`); 85 84 } 86 85 } 87 86 88 87 if (displayName) { 89 88 if (checkProfiles?.displayName === true) { 90 - if (checkProfiles.check.test(displayName)) { 91 - if (checkProfiles.whitelist) { 92 - if (checkProfiles.whitelist.test(displayName)) { 93 - logger.info('Whitelisted phrase found.'); 89 + if (checkProfiles!.check.test(displayName)) { 90 + if (checkProfiles!.whitelist) { 91 + if (checkProfiles!.whitelist.test(displayName)) { 92 + logger.info(`Whitelisted phrase found.`); 94 93 return; 95 94 } 96 95 } else { 97 - logger.info(`${checkProfiles.label} in displayName for ${did}`); 96 + logger.info(`${checkProfiles!.label} in displayName for ${did}`); 98 97 } 99 98 100 - if (checkProfiles.reportOnly === true) { 99 + if (checkProfiles!.reportOnly === true) { 101 100 createAccountReport( 102 101 did, 103 - `${time}: ${checkProfiles.comment} - ${displayName} - ${description}`, 102 + `${time}: ${checkProfiles!.comment} - ${displayName} - ${description}`, 104 103 ); 105 104 return; 106 105 } else { 107 106 createAccountLabel( 108 107 did, 109 - checkProfiles.label, 110 - `${time}: ${checkProfiles.comment}`, 108 + `${checkProfiles!.label}`, 109 + `${time}: ${checkProfiles!.comment}`, 111 110 ); 112 111 } 113 112 }
+1 -1
src/types.ts
··· 39 39 40 40 // Define the type for the link feature 41 41 export interface LinkFeature { 42 - $type: 'app.bsky.richtext.facet#link'; 42 + $type: "app.bsky.richtext.facet#link"; 43 43 uri: string; 44 44 } 45 45
+51 -50
src/utils.ts
··· 1 - import logger from './logger.js'; 1 + import logger from "./logger.js"; 2 + 3 + import { homoglyphMap } from "./homoglyphs"; 2 4 3 - /* Normalize the Unicode characters: this doesn't consistently work yet, there is something about certain bluesky strings that causes it to fail. */ 5 + /** 6 + * Normalizes a string by converting it to lowercase, replacing homoglyphs, 7 + * and stripping diacritics. This is useful for sanitizing user input 8 + * before performing checks for forbidden words. 9 + * 10 + * The process is as follows: 11 + * 1. Convert the entire string to lowercase. 12 + * 2. Replace characters that are visually similar to ASCII letters (homoglyphs) 13 + * with their ASCII counterparts based on the `homoglyphMap`. 14 + * 3. Apply NFD (Normalization Form D) Unicode normalization to decompose 15 + * characters into their base characters and combining marks. 16 + * 4. Remove all Unicode combining diacritical marks. 17 + * 5. Apply NFKC (Normalization Form KC) Unicode normalization for a final 18 + * cleanup, which handles compatibility characters. 19 + * 20 + * @param text The input string to normalize. 21 + * @returns The normalized string. 22 + */ 4 23 export function normalizeUnicode(text: string): string { 5 - // First decompose the characters (NFD) 6 - const decomposed = text.normalize('NFD'); 24 + // Convert to lowercase to match the homoglyph map keys 25 + const lowercased = text.toLowerCase(); 7 26 8 - // Remove diacritics and combining marks 9 - const withoutDiacritics = decomposed.replace(/[\u0300-\u036f]/g, ''); 27 + // Replace characters using the homoglyph map. 28 + // This is done before NFD so that pre-composed characters are caught. 29 + let replaced = ""; 30 + for (const char of lowercased) { 31 + replaced += homoglyphMap[char] || char; 32 + } 10 33 11 - // Remove mathematical alphanumeric symbols 12 - const withoutMath = withoutDiacritics.replace( 13 - /[\uD835][\uDC00-\uDFFF]/g, 14 - (char) => { 15 - // Get the base character from the mathematical symbol 16 - const code = char.codePointAt(0); 17 - if (code >= 0x1d400 && code <= 0x1d433) 18 - // Mathematical bold 19 - return String.fromCharCode(code - 0x1d400 + 0x41); 20 - if (code >= 0x1d434 && code <= 0x1d467) 21 - // Mathematical italic 22 - return String.fromCharCode(code - 0x1d434 + 0x61); 23 - if (code >= 0x1d468 && code <= 0x1d49b) 24 - // Mathematical bold italic 25 - return String.fromCharCode(code - 0x1d468 + 0x41); 26 - if (code >= 0x1d49c && code <= 0x1d4cf) 27 - // Mathematical script 28 - return String.fromCharCode(code - 0x1d49c + 0x61); 29 - return char; 30 - }, 31 - ); 34 + // First decompose the characters (NFD), then remove diacritics. 35 + const withoutDiacritics = replaced 36 + .normalize("NFD") 37 + .replace(/[\u0300-\u036f]/g, ""); 32 38 33 - // Final NFKC normalization to handle any remaining special characters 34 - return withoutMath.normalize('NFKC'); 39 + // Final NFKC normalization to handle any remaining special characters. 40 + return withoutDiacritics.normalize("NFKC"); 35 41 } 36 42 37 43 export async function getFinalUrl(url: string): Promise<string> { 38 44 const controller = new AbortController(); 39 - const timeoutId = setTimeout(() => { controller.abort(); }, 10000); // 10-second timeout 45 + const timeoutId = setTimeout(() => controller.abort(), 10000); // 10-second timeout 40 46 41 47 try { 42 48 const response = await fetch(url, { 43 - method: 'HEAD', 44 - redirect: 'follow', // This will follow redirects automatically 49 + method: "HEAD", 50 + redirect: "follow", // This will follow redirects automatically 45 51 signal: controller.signal, // Pass the abort signal to fetch 46 52 }); 47 53 clearTimeout(timeoutId); // Clear the timeout if fetch completes ··· 49 55 } catch (error) { 50 56 clearTimeout(timeoutId); // Clear the timeout if fetch fails 51 57 // Log the error with more specific information if it's a timeout 52 - if (error instanceof Error && error.name === 'AbortError') { 58 + if (error instanceof Error && error.name === "AbortError") { 53 59 logger.warn(`Timeout fetching URL: ${url}`, error); 54 60 } else { 55 61 logger.warn(`Error fetching URL: ${url}`, error); ··· 59 65 } 60 66 61 67 export async function getLanguage(profile: string): Promise<string> { 62 - if (!profile) { 68 + if (typeof profile !== "string" || profile === null) { 63 69 logger.warn( 64 - '[GETLANGUAGE] getLanguage called with empty profile data, defaulting to \'eng\'.', 70 + "[GETLANGUAGE] getLanguage called with invalid profile data, defaulting to 'eng'.", 65 71 profile, 66 72 ); 67 - return 'eng'; // Default or throw an error 73 + return "eng"; // Default or throw an error 68 74 } 69 75 70 76 const profileText = profile.trim(); 71 77 72 78 if (profileText.length === 0) { 73 - return 'eng'; 79 + return "eng"; 74 80 } 75 81 76 - try { 77 - const lande = (await import('lande')).default; 78 - const langsProbabilityMap = lande(profileText); 82 + const lande = (await import("lande")).default; 83 + let langsProbabilityMap = lande(profileText); 79 84 80 - // Sort by probability in descending order 81 - langsProbabilityMap.sort( 82 - (a: [string, number], b: [string, number]) => b[1] - a[1], 83 - ); 85 + // Sort by probability in descending order 86 + langsProbabilityMap.sort( 87 + (a: [string, number], b: [string, number]) => b[1] - a[1], 88 + ); 84 89 85 - // Return the language code with the highest probability 86 - return langsProbabilityMap[0][0]; 87 - } catch (error) { 88 - logger.error('Error detecting language, defaulting to \'eng\':', error); 89 - return 'eng'; // Fallback to English on error 90 - } 90 + // Return the language code with the highest probability 91 + return langsProbabilityMap[0][0]; 91 92 }
+53 -49
src/validateEnv.ts
··· 1 - import logger from './logger.js'; 1 + import logger from "./logger.js"; 2 2 3 3 interface EnvironmentVariable { 4 4 name: string; ··· 9 9 10 10 const ENV_VARIABLES: EnvironmentVariable[] = [ 11 11 { 12 - name: 'DID', 12 + name: "DID", 13 13 required: true, 14 - description: 'Moderator DID for labeling operations', 15 - validator: (value) => value.startsWith('did:'), 14 + description: "Moderator DID for labeling operations", 15 + validator: (value) => value.startsWith("did:"), 16 16 }, 17 17 { 18 - name: 'OZONE_URL', 18 + name: "OZONE_URL", 19 19 required: true, 20 - description: 'Ozone server URL', 21 - validator: (value) => value.includes('.') && value.length > 3, 20 + description: "Ozone server URL", 21 + validator: (value) => value.includes(".") && value.length > 3, 22 22 }, 23 23 { 24 - name: 'OZONE_PDS', 24 + name: "OZONE_PDS", 25 25 required: true, 26 - description: 'Ozone PDS URL', 27 - validator: (value) => value.includes('.') && value.length > 3, 26 + description: "Ozone PDS URL", 27 + validator: (value) => value.includes(".") && value.length > 3, 28 28 }, 29 29 { 30 - name: 'BSKY_HANDLE', 30 + name: "BSKY_HANDLE", 31 31 required: true, 32 - description: 'Bluesky handle for authentication', 33 - validator: (value) => value.includes('.'), 32 + description: "Bluesky handle for authentication", 33 + validator: (value) => value.includes("."), 34 34 }, 35 35 { 36 - name: 'BSKY_PASSWORD', 36 + name: "BSKY_PASSWORD", 37 37 required: true, 38 - description: 'Bluesky password for authentication', 38 + description: "Bluesky password for authentication", 39 39 validator: (value) => value.length > 0, 40 40 }, 41 41 { 42 - name: 'HOST', 42 + name: "HOST", 43 43 required: false, 44 - description: 'Host address for the server (defaults to 127.0.0.1)', 44 + description: "Host address for the server (defaults to 127.0.0.1)", 45 45 }, 46 46 { 47 - name: 'PORT', 47 + name: "PORT", 48 48 required: false, 49 - description: 'Port for the main server (defaults to 4100)', 49 + description: "Port for the main server (defaults to 4100)", 50 50 validator: (value) => !isNaN(Number(value)) && Number(value) > 0, 51 51 }, 52 52 { 53 - name: 'METRICS_PORT', 53 + name: "METRICS_PORT", 54 54 required: false, 55 - description: 'Port for metrics server (defaults to 4101)', 55 + description: "Port for metrics server (defaults to 4101)", 56 56 validator: (value) => !isNaN(Number(value)) && Number(value) > 0, 57 57 }, 58 58 { 59 - name: 'FIREHOSE_URL', 59 + name: "FIREHOSE_URL", 60 60 required: false, 61 - description: 'Jetstream firehose WebSocket URL', 62 - validator: (value) => value.startsWith('ws'), 61 + description: "Jetstream firehose WebSocket URL", 62 + validator: (value) => value.startsWith("ws"), 63 63 }, 64 64 { 65 - name: 'CURSOR_UPDATE_INTERVAL', 65 + name: "CURSOR_UPDATE_INTERVAL", 66 66 required: false, 67 - description: 'Cursor update interval in milliseconds (defaults to 60000)', 67 + description: "Cursor update interval in milliseconds (defaults to 60000)", 68 68 validator: (value) => !isNaN(Number(value)) && Number(value) > 0, 69 69 }, 70 70 { 71 - name: 'LABEL_LIMIT', 71 + name: "LABEL_LIMIT", 72 72 required: false, 73 - description: 'Rate limit for labeling operations', 73 + description: "Rate limit for labeling operations", 74 74 validator: (value) => { 75 75 // Allow "number * number" format or plain numbers 76 76 const multiplyMatch = /^(\d+)\s*\*\s*(\d+)$/.exec(value); ··· 82 82 }, 83 83 }, 84 84 { 85 - name: 'LABEL_LIMIT_WAIT', 85 + name: "LABEL_LIMIT_WAIT", 86 86 required: false, 87 - description: 'Wait time between rate limited operations', 87 + description: "Wait time between rate limited operations", 88 88 validator: (value) => { 89 89 // Allow "number * number" format or plain numbers 90 90 const multiplyMatch = /^(\d+)\s*\*\s*(\d+)$/.exec(value); ··· 96 96 }, 97 97 }, 98 98 { 99 - name: 'LOG_LEVEL', 99 + name: "LOG_LEVEL", 100 100 required: false, 101 - description: 'Logging level (trace, debug, info, warn, error, fatal)', 101 + description: "Logging level (trace, debug, info, warn, error, fatal)", 102 102 validator: (value) => 103 - ['trace', 'debug', 'info', 'warn', 'error', 'fatal'].includes(value), 103 + ["trace", "debug", "info", "warn", "error", "fatal"].includes(value), 104 104 }, 105 105 { 106 - name: 'NODE_ENV', 106 + name: "NODE_ENV", 107 107 required: false, 108 - description: 'Node environment (development, production, test)', 109 - validator: (value) => ['development', 'production', 'test'].includes(value), 108 + description: "Node environment (development, production, test)", 109 + validator: (value) => ["development", "production", "test"].includes(value), 110 110 }, 111 111 ]; 112 112 ··· 114 114 const errors: string[] = []; 115 115 const warnings: string[] = []; 116 116 117 - logger.info('Validating environment variables...'); 117 + logger.info("Validating environment variables..."); 118 118 119 119 for (const envVar of ENV_VARIABLES) { 120 120 const value = process.env[envVar.name]; 121 121 122 122 if (envVar.required) { 123 - if (!value || value.trim() === '') { 123 + if (!value || value.trim() === "") { 124 124 errors.push( 125 - `Required environment variable ${envVar.name} is missing. ${envVar.description}` 125 + `Required environment variable ${envVar.name} is missing. ${envVar.description}`, 126 126 ); 127 127 continue; 128 128 } ··· 132 132 try { 133 133 if (!envVar.validator(value)) { 134 134 errors.push( 135 - `Environment variable ${envVar.name} has invalid format. ${envVar.description}` 135 + `Environment variable ${envVar.name} has invalid format. ${envVar.description}`, 136 136 ); 137 137 } 138 138 } catch (error) { 139 139 errors.push( 140 - `Environment variable ${envVar.name} validation failed: ${String(error)}. ${envVar.description}` 140 + `Environment variable ${envVar.name} validation failed: ${String(error)}. ${envVar.description}`, 141 141 ); 142 142 } 143 143 } 144 144 145 145 if (!envVar.required && !value) { 146 146 warnings.push( 147 - `Optional environment variable ${envVar.name} not set, using default. ${envVar.description}` 147 + `Optional environment variable ${envVar.name} not set, using default. ${envVar.description}`, 148 148 ); 149 149 } 150 150 } 151 151 152 152 if (warnings.length > 0) { 153 - logger.warn('Environment variable warnings:'); 154 - warnings.forEach((warning) => { logger.warn(` - ${warning}`); }); 153 + logger.warn("Environment variable warnings:"); 154 + warnings.forEach((warning) => { 155 + logger.warn(` - ${warning}`); 156 + }); 155 157 } 156 158 157 159 if (errors.length > 0) { 158 - logger.error('Environment variable validation failed:'); 159 - errors.forEach((error) => { logger.error(` - ${error}`); }); 160 - logger.error('Please check your environment configuration and try again.'); 160 + logger.error("Environment variable validation failed:"); 161 + errors.forEach((error) => { 162 + logger.error(` - ${error}`); 163 + }); 164 + logger.error("Please check your environment configuration and try again."); 161 165 process.exit(1); 162 166 } 163 167 164 - logger.info('Environment variable validation completed successfully'); 165 - } 168 + logger.info("Environment variable validation completed successfully"); 169 + }