PDS Admin tool make it easier to moderate your PDS with labels

done?

+99 -20
+49 -12
README.md
··· 1 - # Label Watcher 2 - 3 - >WIP: Will share with a few people, and ofc it's public if you are reading this. But going run locally before telling the whole atmosphere about it and make a docker container 4 - 5 - 6 - Does what it says. Watches for labels. Okay really it watches for labelers and labels you set. And if it sees a label applied to an account on the PDS it notifies you via email. And will one day do auto take downs as well. 1 + # Label Watcher - PDS Moderation powered by labelers 7 2 8 - The idea is we have some awesome labelers like [skywatch.blue](https://bsky.app/profile/skywatch.blue) and [Hailey's Labeler](https://bsky.app/profile/did:plc:saslbwamakedc4h6c5bmshvz) that are doing an amazing job of catching troubled accounts that PDS admins may want to know if they are doing these things on their PDS. This gives an easy way to use these resources to help moderate your PDS. And once auto takedowns are added I think it will be great for PDSs that also run their own labeler to be able to issue takedowns from a manual label added via ozone. 3 + Subscribes to multiple labelers that you set in your [settings.toml](settings.toml.example) along with which labels you would like to watch for. If it sees the label is applied to a user on one of the PDSs you configure it will take an action. Either notify you by email, or does an auto takedown of the account. 9 4 5 + The idea is we have some awesome labelers like [skywatch.blue](https://bsky.app/profile/skywatch.blue) and [Hailey's Labeler](https://bsky.app/profile/did:plc:saslbwamakedc4h6c5bmshvz) that are doing an amazing job of moderating already, but we do not have a way as PDS admins to be able to use these labels in an easy way. The hope is that this gives an easy way to use these resources to help moderate your PDS. Pick your labelers to subscribe to and which labels from it you would like to to watch for. Then set an action like notify to get an email when a label is applied to an account on your PDS, or can even do an auto takedown of the account(beta and recommend trying the notify action first to get a hang of what accounts gets the label you expect). 10 6 7 + I think it will be great for PDSs that also run their own labeler to be able to issue a label for takedowns allowing moderates of an org to have the ability to remotely do takedowns with out the need of the PDS admin password. Should also work at catching bot accounts since the labelers have gotten very good at it 11 8 12 9 13 10 14 11 # Setup 15 12 Got some configs to setup. Use toml to set it up and have an example one at [settings.toml.example](./settings.toml.example). Here you can set 16 - - Which PDSs to follow 13 + - PDSs settings 14 + - Can set the watch for multiple PDSs 15 + - An array of email addresses to send the notifications to 16 + - On startup should it query your PDS to find all the active accounts and add them to the watch list 17 + - Should it subscribe to your PDS to auto pick up new accounts (cursor resume does not work for this) 18 + - Admin password. This is the keys to your PDS so please use this with caution. label watcher does not require this. But it is needed for auto takedowns. 17 19 - Which labelers and labels 20 + - Can set multiple labelers 21 + - Backfill (should mostly be supported in my test) 22 + - Which label and which action to take when it is seen 18 23 19 - Also have a .env for some shared secrets at [.env.example](.env.example) 24 + Also have a .env for some shared secrets at [.env.example](.env.example). 20 25 21 26 22 - Can use pnpm or npm and just do 27 + Can use pnpm or npm and run with 23 28 ```bash 24 29 pnpm i 25 30 pnpm run start 26 31 ``` 32 + or can use the docker compose file with 33 + ```bash 34 + docker compose up 35 + ``` 36 + 37 + # How do I find the labeler info? 38 + 1. Find a labeler you like. Like [@skywatch.blue](https://bsky.app/profile/skywatch.blue) 39 + 2. Resolve it's did doc or use one of the atproto browsers to get the `atproto_labeler` service endpoint. sky watches is `https://ozone.skywatch.blue/` so `ozone.skywatch.blue` is the host 40 + 3. Each labeler has a record at `app.bsky.labeler.service` with a rkey of self. Like [here](https://blewit.us-west.host.bsky.network/xrpc/com.atproto.repo.getRecord?repo=did:plc:e4elbtctnfqocyfcml6h2lf7&collection=app.bsky.labeler.service&rkey=self). 41 + 4. The label values are found under policies -> labelValues. We are going to use `bluesky-elder` as an example and set an action of `notify`. 42 + 43 + Put it all together it looks like this 44 + ```toml 45 + # Define the labeler 46 + [labeler.skywatch] 47 + host = "ozone.skywatch.blue" 48 + # Set if you want to replay labels. Takes a minute, or a few... 49 + # backfillLabels = true 50 + 51 + # Notifies if an account has the bluesky-elder label applied 52 + [labeler.skywatch.labels.bluesky-elder] 53 + label_name = "bluesky-elder" 54 + action = "notify" 55 + 56 + #Repeat the last one for as many labels as you want 57 + ``` 27 58 28 59 29 60 # Features ··· 38 69 ## Labeler 39 70 - Can subscribe to labels from a labeler. Multiple of each set up in the settings.toml 40 71 - Does support backfilling of labels via the cursor on restart so you do not miss any 41 - - Will support full backfill at some point 42 - - Can give each label an action like notify or takedown (will come later). It will take the action and send you an email 72 + - Should support full backfill if set 73 + - Can give each label an action like notify or takedown. It will take the action and send you an email 74 + - Auto takedowns are still new and should expect possible bugs. Takedowns have been reservable in my experience, but I highly recommend to start with notify first and do manual takedowns when needed while you find what labels work. 75 + - During auto takedowns if a label is negated(reversed) the takedown is also reversed 76 + 77 + 78 + # Future features? 79 + Not sure. Playing around with making a UI possibly, maybe even a multi instance that others can sign up for and not have to host?
+21
settings.toml.example
··· 11 11 # Listens for new accounts 12 12 listenForNewAccounts = true 13 13 14 + # Another PDS you want to watch 15 + # [pds.your-pds-two] 16 + # host = "your-pds-two.com" 17 + # notifyEmails = ["admin@example.com", "another@example.com"] 18 + # # Will be used for auto take downs on down the road 19 + # pdsAdminPassword = "secure" 20 + # # Loads all the historic accounts in 21 + # backfillAccounts = true 22 + # # Listens for new accounts 23 + # listenForNewAccounts = true 24 + 14 25 15 26 # Define the labeler. Can do multiple labelers 16 27 [labeler.skywatch] ··· 25 36 [labeler.skywatch.labels.platform-manipulation] 26 37 label_name = "platform-manipulation" 27 38 action = "notify" 39 + 40 + 41 + # Define another labeler 42 + # [labeler.hailey] 43 + # host = "ozone.hailey.at" 44 + 45 + # 46 + # [labeler.hailey.labels.suss-handle-change] 47 + # label_name = "suss-handle-change" 48 + # action = "notify"
+21 -7
src/handlers/handleNewLabel.ts
··· 136 136 ); 137 137 break; 138 138 case "takedown": { 139 - let takedownSuccess: boolean; 139 + // Can be a successful takedown or not 140 + let takedownActionSucceededs: boolean; 140 141 141 142 if (pdsConfig.pdsAdminPassword) { 142 143 const rpc = new Client({ ··· 160 161 }, 161 162 headers: adminAuthHeader(pdsConfig.pdsAdminPassword), 162 163 }); 164 + 165 + await db 166 + .update(schema.watchedRepos) 167 + .set({ 168 + takeDownIssuedDate: null, 169 + }) 170 + .where(eq(schema.watchedRepos.did, targetDid)); 171 + 163 172 logger.info( 164 173 { did: targetDid }, 165 174 "Takedown reversed successfully", 166 175 ); 176 + takedownActionSucceededs = true; 167 177 } else { 168 178 if (!watchedRepo.takeDownIssuedDate) { 169 179 logger.info({ did: targetDid }, "Issuing takedown"); ··· 180 190 }, 181 191 headers: adminAuthHeader(pdsConfig.pdsAdminPassword), 182 192 }); 183 - await db.update(schema.watchedRepos).set({ 184 - takeDownIssuedDate: new Date(), 185 - }); 193 + 194 + await db 195 + .update(schema.watchedRepos) 196 + .set({ 197 + takeDownIssuedDate: new Date(), 198 + }) 199 + .where(eq(schema.watchedRepos.did, targetDid)); 186 200 187 201 logger.info( 188 202 { did: targetDid }, 189 203 "Takedown issued successfully", 190 204 ); 191 - takedownSuccess = true; 205 + takedownActionSucceededs = true; 192 206 } else { 193 207 logger.info( 194 208 { did: targetDid }, ··· 197 211 } 198 212 } 199 213 } catch (err) { 200 - takedownSuccess = false; 214 + takedownActionSucceededs = false; 201 215 logger.error( 202 216 { err, did: targetDid }, 203 217 label.neg ··· 222 236 dateApplied: labledDate, 223 237 takeDown: true, 224 238 targetUri: label.uri, 225 - takedownSuccess, 239 + takedownSuccess: takedownActionSucceededs, 226 240 }).catch((err) => 227 241 logger.error( 228 242 { err },
+8 -1
src/index.ts
··· 1 1 import { db } from "./db/index.js"; 2 2 import { migrate } from "drizzle-orm/libsql/migrator"; 3 - import { readFileSync } from "node:fs"; 3 + import { existsSync, readFileSync } from "node:fs"; 4 4 import { parse } from "smol-toml"; 5 5 import PQueue from "p-queue"; 6 6 import { labelerSubscriber } from "./handlers/lablerSubscriber.js"; ··· 17 17 18 18 // Run Drizzle migrations on startup 19 19 migrate(db, { migrationsFolder: process.env.MIGRATIONS_FOLDER ?? "drizzle" }); 20 + 21 + if (!existsSync("./settings.toml")) { 22 + logger.error( 23 + "Error: settings.toml not found. Please create one from settings.toml.example", 24 + ); 25 + process.exit(1); 26 + } 20 27 21 28 const settingsFile = readFileSync("./settings.toml", "utf-8"); 22 29