A tool for parsing traffic on the jetstream and applying a moderation workstream based on regexp based rules
at main 5.7 kB view raw
1import { agent, isLoggedIn } from "./agent.js"; 2import { MOD_DID } from "./config.js"; 3import { limit } from "./limits.js"; 4import { logger } from "./logger.js"; 5import { labelsAppliedCounter, labelsCachedCounter } from "./metrics.js"; 6import { tryClaimPostLabel } from "./redis.js"; 7 8const doesLabelExist = ( 9 labels: { val: string }[] | undefined, 10 labelVal: string, 11): boolean => { 12 if (!labels) { 13 return false; 14 } 15 return labels.some((label) => label.val === labelVal); 16}; 17 18export const createPostLabel = async ( 19 uri: string, 20 cid: string, 21 label: string, 22 comment: string, 23 duration: number | undefined, 24 did?: string, 25 time?: number, 26) => { 27 await isLoggedIn; 28 29 const claimed = await tryClaimPostLabel(uri, label); 30 if (!claimed) { 31 logger.debug( 32 { process: "MODERATION", uri, label }, 33 "Post label already claimed in Redis, skipping", 34 ); 35 labelsCachedCounter.inc({ 36 label_type: label, 37 target_type: "post", 38 reason: "redis_cache", 39 }); 40 return; 41 } 42 43 const hasLabel = await checkRecordLabels(uri, label); 44 if (hasLabel) { 45 logger.debug( 46 { process: "MODERATION", uri, label }, 47 "Post already has label, skipping", 48 ); 49 labelsCachedCounter.inc({ 50 label_type: label, 51 target_type: "post", 52 reason: "existing_label", 53 }); 54 return; 55 } 56 57 logger.info( 58 { process: "MODERATION", label, did, atURI: uri }, 59 "Labeling post", 60 ); 61 labelsAppliedCounter.inc({ label_type: label, target_type: "post" }); 62 63 await limit(async () => { 64 try { 65 const event: { 66 $type: string; 67 comment: string; 68 createLabelVals: string[]; 69 negateLabelVals: string[]; 70 durationInHours?: number; 71 } = { 72 $type: "tools.ozone.moderation.defs#modEventLabel", 73 comment, 74 createLabelVals: [label], 75 negateLabelVals: [], 76 }; 77 78 if (duration) { 79 event.durationInHours = duration; 80 } 81 82 await agent.tools.ozone.moderation.emitEvent( 83 { 84 event, 85 // specify the labeled post by strongRef 86 subject: { 87 $type: "com.atproto.repo.strongRef", 88 uri, 89 cid, 90 }, 91 // put in the rest of the metadata 92 createdBy: agent.did ?? "", 93 createdAt: new Date().toISOString(), 94 modTool: { 95 name: "skywatch/skywatch-automod", 96 meta: { 97 time: new Date().toISOString(), 98 externalUrl: `https://pdsls.dev/${uri}`, 99 }, 100 }, 101 }, 102 { 103 encoding: "application/json", 104 headers: { 105 "atproto-proxy": `${MOD_DID}#atproto_labeler`, 106 "atproto-accept-labelers": 107 "did:plc:ar7c4by46qjdydhdevvrndac;redact", 108 }, 109 }, 110 ); 111 112 if (did && time) { 113 try { 114 // Dynamic import to avoid circular dependency: 115 // accountThreshold imports from moderation (createAccountLabel, etc.) 116 // moderation imports from accountThreshold (checkAccountThreshold) 117 const { checkAccountThreshold } = await import( 118 "./accountThreshold.js" 119 ); 120 await checkAccountThreshold(did, uri, label, time); 121 } catch (error) { 122 logger.error( 123 { process: "ACCOUNT_THRESHOLD", did, label, error }, 124 "Failed to check account threshold", 125 ); 126 } 127 } 128 } catch (e) { 129 logger.error( 130 { process: "MODERATION", error: e }, 131 "Failed to create post label", 132 ); 133 throw e; 134 } 135 }); 136}; 137 138export const createPostReport = async ( 139 uri: string, 140 cid: string, 141 comment: string, 142) => { 143 await isLoggedIn; 144 await limit(async () => { 145 try { 146 return await agent.tools.ozone.moderation.emitEvent( 147 { 148 event: { 149 $type: "tools.ozone.moderation.defs#modEventReport", 150 comment, 151 reportType: "com.atproto.moderation.defs#reasonOther", 152 }, 153 // specify the labeled post by strongRef 154 subject: { 155 $type: "com.atproto.repo.strongRef", 156 uri, 157 cid, 158 }, 159 // put in the rest of the metadata 160 createdBy: agent.did ?? "", 161 createdAt: new Date().toISOString(), 162 modTool: { 163 name: "skywatch/skywatch-automod", 164 meta: { 165 time: new Date().toISOString(), 166 externalUrl: `https://pdsls.dev/${uri}`, 167 }, 168 }, 169 }, 170 { 171 encoding: "application/json", 172 headers: { 173 "atproto-proxy": `${MOD_DID}#atproto_labeler`, 174 "atproto-accept-labelers": 175 "did:plc:ar7c4by46qjdydhdevvrndac;redact", 176 }, 177 }, 178 ); 179 } catch (e) { 180 logger.error( 181 { process: "MODERATION", error: e }, 182 "Failed to create post report", 183 ); 184 throw e; 185 } 186 }); 187}; 188 189export const checkRecordLabels = async ( 190 uri: string, 191 label: string, 192): Promise<boolean> => { 193 await isLoggedIn; 194 return await limit(async () => { 195 try { 196 const response = await agent.tools.ozone.moderation.getRecord( 197 { uri }, 198 { 199 headers: { 200 "atproto-proxy": `${MOD_DID}#atproto_labeler`, 201 "atproto-accept-labelers": 202 "did:plc:ar7c4by46qjdydhdevvrndac;redact", 203 }, 204 }, 205 ); 206 207 return doesLabelExist(response.data.labels, label); 208 } catch (e) { 209 logger.error( 210 { process: "MODERATION", uri, error: e }, 211 "Failed to check record labels", 212 ); 213 return false; 214 } 215 }); 216};