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

Merge pull request #1 from skywatch-bsky/dev-110

Aggregate Updates

authored by

Scarnecchia and committed by
GitHub
bb1f1661 3c9d1629

+240 -48
+17 -6
src/checkHandles.ts
··· 4 4 import { 5 5 createAccountReport, 6 6 createAccountLabel, 7 - createAccountComment, 7 + checkAccountLabels, 8 8 } from "./moderation.js"; 9 + import { limit } from "./limits.js"; 9 10 10 11 export const checkHandle = async (handle: Handle[]) => { 12 + const ActLabelChk = await limit(() => checkAccountLabels(handle[0].did)); 11 13 // Get a list of labels 12 14 const labels: string[] = Array.from( 13 15 HANDLE_CHECKS, ··· 44 46 ); 45 47 return; 46 48 } else { 47 - createAccountLabel( 48 - handle[0].did, 49 - `${checkList!.label}`, 50 - `${handle[0].time}: ${checkList!.comment} - ${handle[0].handle}`, 51 - ); 49 + if (ActLabelChk) { 50 + if (ActLabelChk.includes(checkList!.label)) { 51 + logger.info( 52 + `Label ${checkList!.label} already exists for ${did}`, 53 + ); 54 + return; 55 + } else { 56 + createAccountLabel( 57 + handle[0].did, 58 + `${checkList!.label}`, 59 + `${handle[0].time}: ${checkList!.comment} - ${handle[0].handle}`, 60 + ); 61 + } 62 + } 52 63 } 53 64 } 54 65 }
+1 -1
src/checkPosts.ts
··· 61 61 `${post[0].time}: ${checkPost?.comment} at ${post[0].atURI} with text "${post[0].text}"`, 62 62 ); 63 63 return; 64 - } else if (checkPost?.label === "fundraising-link") { 64 + } else if (checkPost?.label === "fundraising-link" || checkPost?.label === "twitter-x") { 65 65 return; // skip fundraising links—hardcoded because of the insane volume by spammers. 66 66 } else if (checkPost!.commentOnly === false) { 67 67 logger.info(
+91 -32
src/checkProfiles.ts
··· 1 + import { describe } from "node:test"; 1 2 import { PROFILE_CHECKS } from "./constants.js"; 2 3 import logger from "./logger.js"; 3 4 import { 4 5 createAccountReport, 5 6 createAccountLabel, 6 - createAccountComment, 7 + checkAccountLabels, 7 8 } from "./moderation.js"; 9 + import { limit } from "./limits.js"; 8 10 9 - export const checkProfile = async ( 11 + export const checkDescription = async ( 10 12 did: string, 11 13 time: number, 12 14 displayName: string, 13 15 description: string, 14 16 ) => { 17 + const ActLabelChk = await limit(() => checkAccountLabels(did)); 15 18 // Get a list of labels 16 19 const labels: string[] = Array.from( 17 20 PROFILE_CHECKS, ··· 29 32 if (checkProfiles.ignoredDIDs.includes(did)) { 30 33 return logger.info(`Whitelisted DID: ${did}`); 31 34 } 32 - } else { 33 - let checkCount: number = 0; // Counter for checking if any checks are found 35 + } 34 36 35 - // Check if description is enabled 37 + if (description) { 36 38 if (checkProfiles?.description === true) { 37 39 if (checkProfiles!.check.test(description)) { 38 - if (checkProfiles?.whitelist) { 39 - if (checkProfiles?.whitelist.test(description)) { 40 + if (checkProfiles!.whitelist) { 41 + if (checkProfiles!.whitelist.test(description)) { 40 42 logger.info(`Whitelisted phrase found.`); 43 + return; 41 44 } 42 45 } else { 43 - logger.info(`${checkProfiles!.label} in description.`); 44 - checkCount++; 46 + logger.info(`${checkProfiles!.label} in description for ${did}`); 47 + } 48 + 49 + if (checkProfiles!.reportOnly === true) { 50 + createAccountReport( 51 + did, 52 + `${time}: ${checkProfiles!.comment} - ${displayName} - ${description}`, 53 + ); 54 + return; 55 + } else { 56 + if (ActLabelChk) { 57 + if (ActLabelChk.includes(checkProfiles!.label)) { 58 + logger.info( 59 + `Label ${checkProfiles!.label} already exists for ${did}`, 60 + ); 61 + return; 62 + } 63 + } else { 64 + createAccountLabel( 65 + did, 66 + `${checkProfiles!.label}`, 67 + `${time}: ${checkProfiles!.comment} - ${displayName} - ${description}`, 68 + ); 69 + } 45 70 } 46 71 } 47 72 } 73 + } 74 + }); 75 + }; 48 76 77 + export const checkDisplayName = async ( 78 + did: string, 79 + time: number, 80 + displayName: string, 81 + description: string, 82 + ) => { 83 + const ActLabelChk = await limit(() => checkAccountLabels(did)); 84 + // Get a list of labels 85 + const labels: string[] = Array.from( 86 + PROFILE_CHECKS, 87 + (profileCheck) => profileCheck.label, 88 + ); 89 + 90 + // iterate through the labels 91 + labels.forEach((label) => { 92 + const checkProfiles = PROFILE_CHECKS.find( 93 + (profileCheck) => profileCheck.label === label, 94 + ); 95 + 96 + // Check if DID is whitelisted 97 + if (checkProfiles?.ignoredDIDs) { 98 + if (checkProfiles.ignoredDIDs.includes(did)) { 99 + return logger.info(`Whitelisted DID: ${did}`); 100 + } 101 + } 102 + 103 + if (displayName) { 49 104 if (checkProfiles?.displayName === true) { 50 105 if (checkProfiles!.check.test(displayName)) { 51 - if (checkProfiles?.whitelist) { 52 - if (checkProfiles?.whitelist.test(displayName)) { 53 - logger.info(`Whitelisted phrase found for: ${displayName}`); 106 + if (checkProfiles!.whitelist) { 107 + if (checkProfiles!.whitelist.test(displayName)) { 108 + logger.info(`Whitelisted phrase found.`); 109 + return; 54 110 } 55 111 } else { 56 - logger.info( 57 - `${checkProfiles!.label} in display name: ${displayName}`, 112 + logger.info(`${checkProfiles!.label} in displayName for ${did}`); 113 + } 114 + 115 + if (checkProfiles!.reportOnly === true) { 116 + createAccountReport( 117 + did, 118 + `${time}: ${checkProfiles!.comment} - ${displayName} - ${description}`, 58 119 ); 59 - checkCount++; 120 + return; 121 + } else { 122 + if (ActLabelChk) { 123 + if (ActLabelChk.includes(checkProfiles!.label)) { 124 + logger.info( 125 + `Label ${checkProfiles!.label} already exists for ${did}`, 126 + ); 127 + return; 128 + } 129 + } else { 130 + createAccountLabel( 131 + did, 132 + `${checkProfiles!.label}`, 133 + `${time}: ${checkProfiles!.comment} - ${displayName} - ${description}`, 134 + ); 135 + } 60 136 } 61 137 } 62 - } 63 - 64 - if (checkCount === 0) return; 65 - 66 - if (checkProfiles.reportOnly === true) { 67 - logger.info(`Report only: ${did}`); 68 - createAccountReport( 69 - did, 70 - `${time}: ${checkProfiles!.comment} - ${displayName} - ${description}`, 71 - ); 72 - return; 73 - } else { 74 - createAccountLabel( 75 - did, 76 - `${checkProfiles!.label}`, 77 - `${time}: ${checkProfiles!.comment}`, 78 - ); 79 138 } 80 139 } 81 140 });
+42
src/checkStarterPack.ts
··· 1 + import { PROFILE_CHECKS } from "./constants.js"; 2 + import logger from "./logger.js"; 3 + import { createAccountLabel } from "./moderation.js"; 4 + 5 + export const checkStarterPack = async ( 6 + did: string, 7 + time: number, 8 + atURI: string, 9 + ) => { 10 + // Get a list of labels 11 + const labels: string[] = Array.from( 12 + PROFILE_CHECKS, 13 + (profileCheck) => profileCheck.label, 14 + ); 15 + 16 + // iterate through the labels 17 + labels.forEach((label) => { 18 + const checkProfiles = PROFILE_CHECKS.find( 19 + (profileCheck) => profileCheck.label === label, 20 + ); 21 + 22 + // Check if DID is whitelisted 23 + if (checkProfiles?.ignoredDIDs) { 24 + if (checkProfiles.ignoredDIDs.includes(did)) { 25 + return logger.info(`Whitelisted DID: ${did}`); 26 + } 27 + } 28 + 29 + if (atURI) { 30 + if (checkProfiles?.starterPacks) { 31 + if (checkProfiles?.starterPacks.includes(atURI)) { 32 + logger.info(`Account joined via starter pack at: ${atURI}`); 33 + createAccountLabel( 34 + did, 35 + `${checkProfiles!.label}`, 36 + `${time}: ${checkProfiles!.comment} - Account joined via starter pack at: ${atURI}`, 37 + ); 38 + } 39 + } 40 + } 41 + }); 42 + };
+4
src/lists.ts
··· 2 2 3 3 export const LISTS: List[] = [ 4 4 { 5 + label: "blue-heart-emoji", 6 + rkey: "3lfbtgosyyi22", 7 + }, 8 + { 5 9 label: "troll", 6 10 rkey: "3lbckxhgu3r2v", 7 11 },
+55 -9
src/main.ts
··· 1 1 import { 2 2 CommitCreateEvent, 3 + CommitUpdate, 3 4 CommitUpdateEvent, 4 5 IdentityEvent, 5 6 Jetstream, ··· 17 18 import { Post, LinkFeature, Handle } from "./types.js"; 18 19 import { checkPosts } from "./checkPosts.js"; 19 20 import { checkHandle } from "./checkHandles.js"; 20 - import { checkProfile } from "./checkProfiles.js"; 21 + import { checkStarterPack } from "./checkStarterPack.js"; 22 + import { checkDescription, checkDisplayName } from "./checkProfiles.js"; 21 23 22 24 let cursor = 0; 23 25 let cursorUpdateInterval: NodeJS.Timeout; ··· 78 80 79 81 jetstream.onCreate( 80 82 "app.bsky.feed.post", 81 - (event: CommitCreateEvent<typeof WANTED_COLLECTION>) => { 83 + (event: CommitCreateEvent<"app.bsky.feed.post">) => { 82 84 const atURI = `at://${event.did}/app.bsky.feed.post/${event.commit.rkey}`; 83 85 const hasFacets = event.commit.record.hasOwnProperty("facets"); 84 86 const hasText = event.commit.record.hasOwnProperty("text"); ··· 135 137 // Check for profile updates 136 138 jetstream.onUpdate( 137 139 "app.bsky.actor.profile", 138 - (event: CommitUpdateEvent<typeof WANTED_COLLECTION>) => { 140 + async (event: CommitUpdateEvent<"app.bsky.actor.profile">) => { 139 141 try { 140 - const ret = checkProfile( 141 - event.did, 142 - event.time_us, 143 - event.commit.record.displayName, 144 - event.commit.record.description, 145 - ); 142 + if (event.commit.record.displayName || event.commit.record.description) { 143 + checkDescription( 144 + event.did, 145 + event.time_us, 146 + event.commit.record.displayName, 147 + event.commit.record.description, 148 + ); 149 + checkDisplayName( 150 + event.did, 151 + event.time_us, 152 + event.commit.record.displayName, 153 + event.commit.record.description, 154 + ); 155 + } 156 + 157 + if (event.commit.record.joinedViaStarterPack) { 158 + checkStarterPack( 159 + event.did, 160 + event.time_us, 161 + event.commit.record.joinedViaStarterPack.uri, 162 + ); 163 + } 164 + } catch (error) { 165 + logger.error(`Error checking profile: ${error}`); 166 + } 167 + }, 168 + ); 169 + 170 + // Check for profile updates 171 + jetstream.onCreate( 172 + "app.bsky.actor.profile", 173 + async (event: CommitCreateEvent<"app.bsky.actor.profile">) => { 174 + try { 175 + if (event.commit.record.displayName || event.commit.record.description) { 176 + checkDescription( 177 + event.did, 178 + event.time_us, 179 + event.commit.record.displayName, 180 + event.commit.record.description, 181 + ); 182 + checkDisplayName( 183 + event.did, 184 + event.time_us, 185 + event.commit.record.displayName, 186 + event.commit.record.description, 187 + ); 188 + event.commit.record.joinedViaStarterPack?.uri; 189 + } else { 190 + return; 191 + } 146 192 } catch (error) { 147 193 logger.error(`Error checking profile: ${error}`); 148 194 }
+29
src/moderation.ts
··· 185 185 } 186 186 }); 187 187 }; 188 + 189 + export async function checkAccountLabels(did: string) { 190 + /* try { 191 + const repo = await limit(() => 192 + agent.tools.ozone.moderation.getRepo( 193 + { 194 + did: did, 195 + }, 196 + { 197 + headers: { 198 + "atproto-proxy": `${MOD_DID!}#atproto_labeler`, 199 + "atproto-accept-labelers": 200 + "did:plc:ar7c4by46qjdydhdevvrndac;redact", 201 + }, 202 + }, 203 + ), 204 + ); 205 + 206 + if (!repo.data.labels) { 207 + return null; 208 + } 209 + 210 + return repo.data.labels.map((label) => label.label); 211 + } catch (e) { 212 + logger.info("Error retrieving repo for account."); 213 + return null; 214 + } */ 215 + return null; 216 + }
+1
src/types.ts
··· 8 8 check: RegExp; 9 9 whitelist?: RegExp; 10 10 ignoredDIDs?: string[]; 11 + starterPacks?: string[]; 11 12 } 12 13 13 14 export interface Post {