A tool for parsing traffic on the jetstream and applying a moderation workstream based on regexp based rules
at main 8.8 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 { 6 labelsAppliedCounter, 7 labelsCachedCounter, 8 unlabelsRemovedCounter, 9} from "./metrics.js"; 10import { 11 deleteAccountLabelClaim, 12 tryClaimAccountComment, 13 tryClaimAccountLabel, 14} from "./redis.js"; 15 16const doesLabelExist = ( 17 labels: { val: string }[] | undefined, 18 labelVal: string, 19): boolean => { 20 if (!labels) { 21 return false; 22 } 23 return labels.some((label) => label.val === labelVal); 24}; 25 26export const createAccountLabel = async ( 27 did: string, 28 label: string, 29 comment: string, 30) => { 31 await isLoggedIn; 32 33 const claimed = await tryClaimAccountLabel(did, label); 34 if (!claimed) { 35 logger.debug( 36 { process: "MODERATION", did, label }, 37 "Account label already claimed in Redis, skipping", 38 ); 39 labelsCachedCounter.inc({ 40 label_type: label, 41 target_type: "account", 42 reason: "redis_cache", 43 }); 44 return; 45 } 46 47 const hasLabel = await checkAccountLabels(did, label); 48 if (hasLabel) { 49 logger.debug( 50 { process: "MODERATION", did, label }, 51 "Account already has label, skipping", 52 ); 53 labelsCachedCounter.inc({ 54 label_type: label, 55 target_type: "account", 56 reason: "existing_label", 57 }); 58 return; 59 } 60 61 logger.info({ process: "MODERATION", did, label }, "Labeling account"); 62 labelsAppliedCounter.inc({ label_type: label, target_type: "account" }); 63 64 await limit(async () => { 65 try { 66 await agent.tools.ozone.moderation.emitEvent( 67 { 68 event: { 69 $type: "tools.ozone.moderation.defs#modEventLabel", 70 comment, 71 createLabelVals: [label], 72 negateLabelVals: [], 73 }, 74 // specify the labeled post by strongRef 75 subject: { 76 $type: "com.atproto.admin.defs#repoRef", 77 did, 78 }, 79 // put in the rest of the metadata 80 createdBy: agent.did ?? "", 81 createdAt: new Date().toISOString(), 82 modTool: { 83 name: "skywatch/skywatch-automod", 84 meta: { 85 time: new Date().toISOString(), 86 externalUrl: `https://pdsls.dev/at://${did}`, 87 }, 88 }, 89 }, 90 { 91 encoding: "application/json", 92 headers: { 93 "atproto-proxy": `${MOD_DID}#atproto_labeler`, 94 "atproto-accept-labelers": 95 "did:plc:ar7c4by46qjdydhdevvrndac;redact", 96 }, 97 }, 98 ); 99 } catch (e) { 100 logger.error( 101 { process: "MODERATION", error: e }, 102 "Failed to create account label", 103 ); 104 throw e; 105 } 106 }); 107}; 108 109export const createAccountComment = async ( 110 did: string, 111 comment: string, 112 atURI: string, 113) => { 114 await isLoggedIn; 115 116 const claimed = await tryClaimAccountComment(did, atURI); 117 if (!claimed) { 118 logger.debug( 119 { process: "MODERATION", did, atURI }, 120 "Account comment already claimed in Redis, skipping", 121 ); 122 return; 123 } 124 125 logger.info({ process: "MODERATION", did, atURI }, "Commenting on account"); 126 127 await limit(async () => { 128 try { 129 await agent.tools.ozone.moderation.emitEvent( 130 { 131 event: { 132 $type: "tools.ozone.moderation.defs#modEventComment", 133 comment, 134 }, 135 // specify the labeled post by strongRef 136 subject: { 137 $type: "com.atproto.admin.defs#repoRef", 138 did, 139 }, 140 // put in the rest of the metadata 141 createdBy: agent.did ?? "", 142 createdAt: new Date().toISOString(), 143 modTool: { 144 name: "skywatch/skywatch-automod", 145 meta: { 146 time: new Date().toISOString(), 147 externalUrl: `https://pdsls.dev/at://${did}`, 148 }, 149 }, 150 }, 151 { 152 encoding: "application/json", 153 headers: { 154 "atproto-proxy": `${MOD_DID}#atproto_labeler`, 155 "atproto-accept-labelers": 156 "did:plc:ar7c4by46qjdydhdevvrndac;redact", 157 }, 158 }, 159 ); 160 } catch (e) { 161 logger.error( 162 { process: "MODERATION", error: e }, 163 "Failed to create account comment", 164 ); 165 throw e; 166 } 167 }); 168}; 169 170export const createAccountReport = async (did: string, comment: string) => { 171 await isLoggedIn; 172 await limit(async () => { 173 try { 174 await agent.tools.ozone.moderation.emitEvent( 175 { 176 event: { 177 $type: "tools.ozone.moderation.defs#modEventReport", 178 comment, 179 reportType: "com.atproto.moderation.defs#reasonOther", 180 }, 181 // specify the labeled post by strongRef 182 subject: { 183 $type: "com.atproto.admin.defs#repoRef", 184 did, 185 }, 186 // put in the rest of the metadata 187 createdBy: agent.did ?? "", 188 createdAt: new Date().toISOString(), 189 modTool: { 190 name: "skywatch/skywatch-automod", 191 meta: { 192 time: new Date().toISOString(), 193 externalUrl: `https://pdsls.dev/at://${did}`, 194 }, 195 }, 196 }, 197 { 198 encoding: "application/json", 199 headers: { 200 "atproto-proxy": `${MOD_DID}#atproto_labeler`, 201 "atproto-accept-labelers": 202 "did:plc:ar7c4by46qjdydhdevvrndac;redact", 203 }, 204 }, 205 ); 206 } catch (e) { 207 logger.error( 208 { process: "MODERATION", error: e }, 209 "Failed to create account report", 210 ); 211 throw e; 212 } 213 }); 214}; 215 216export const negateAccountLabel = async ( 217 did: string, 218 label: string, 219 comment: string, 220) => { 221 await isLoggedIn; 222 223 const hasLabel = await checkAccountLabels(did, label); 224 if (!hasLabel) { 225 logger.debug( 226 { process: "MODERATION", did, label }, 227 "Account does not have label, skipping", 228 ); 229 return; 230 } 231 232 logger.info({ process: "MODERATION", did, label }, "Unlabeling account"); 233 unlabelsRemovedCounter.inc({ label_type: label, target_type: "account" }); 234 235 await limit(async () => { 236 try { 237 await agent.tools.ozone.moderation.emitEvent( 238 { 239 event: { 240 $type: "tools.ozone.moderation.defs#modEventLabel", 241 comment, 242 createLabelVals: [], 243 negateLabelVals: [label], 244 }, 245 // specify the labeled post by strongRef 246 subject: { 247 $type: "com.atproto.admin.defs#repoRef", 248 did, 249 }, 250 // put in the rest of the metadata 251 createdBy: agent.did ?? "", 252 createdAt: new Date().toISOString(), 253 modTool: { 254 name: "skywatch/skywatch-automod", 255 meta: { 256 time: new Date().toISOString(), 257 externalUrl: `https://pdsls.dev/at://${did}`, 258 }, 259 }, 260 }, 261 { 262 encoding: "application/json", 263 headers: { 264 "atproto-proxy": `${MOD_DID}#atproto_labeler`, 265 "atproto-accept-labelers": 266 "did:plc:ar7c4by46qjdydhdevvrndac;redact", 267 }, 268 }, 269 ); 270 await deleteAccountLabelClaim(did, label); 271 } catch (e) { 272 logger.error( 273 { process: "MODERATION", error: e }, 274 "Failed to negate account label", 275 ); 276 throw e; 277 } 278 }); 279}; 280 281export const checkAccountLabels = async ( 282 did: string, 283 label: string, 284): Promise<boolean> => { 285 await isLoggedIn; 286 return await limit(async () => { 287 try { 288 const response = await agent.tools.ozone.moderation.getRepo( 289 { did }, 290 { 291 headers: { 292 "atproto-proxy": `${MOD_DID}#atproto_labeler`, 293 "atproto-accept-labelers": 294 "did:plc:ar7c4by46qjdydhdevvrndac;redact", 295 }, 296 }, 297 ); 298 299 return doesLabelExist(response.data.labels, label); 300 } catch (e) { 301 logger.error( 302 { process: "MODERATION", did, error: e }, 303 "Failed to check account labels", 304 ); 305 return false; 306 } 307 }); 308}; 309 310export const getAllAccountLabels = async (did: string): Promise<string[]> => { 311 await isLoggedIn; 312 return await limit(async () => { 313 try { 314 const response = await agent.tools.ozone.moderation.getRepo( 315 { did }, 316 { 317 headers: { 318 "atproto-proxy": `${MOD_DID}#atproto_labeler`, 319 "atproto-accept-labelers": 320 "did:plc:ar7c4by46qjdydhdevvrndac;redact", 321 }, 322 }, 323 ); 324 325 return (response.data.labels ?? []).map((label) => label.val); 326 } catch (e) { 327 logger.error( 328 { process: "MODERATION", did, error: e }, 329 "Failed to get account labels", 330 ); 331 return []; 332 } 333 }); 334};