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

Add optional support for webhook notifications #1

(hi Bailey, thanks for this tool!)

Added a small webhook handler to support sending notifications through webhooks instead of SMTP/email. This is optional and webhooks can be configured on a PDS level.

Webhook will transmit the event information and a content key with a descriptive message (like the one that gets emailed) on a JSON payload.

Labels

None yet.

assignee

None yet.

Participants 2
AT URI
at://did:plc:3nlkmby2zllrhcj6z5dnicui/sh.tangled.repo.pull/3mfn5jasl5d22
+124 -39
Diff #0
+2 -1
README.md
··· 1 1 # Label Watcher - PDS Moderation powered by labelers 2 2 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. 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 (and/or webhook), or does an auto takedown of the account. 4 4 5 5 The idea is we have some awesome labelers like [Blacksky's labeler](https://bsky.app/profile/moderation.blacksky.app), [Hailey's Labeler](https://bsky.app/profile/did:plc:saslbwamakedc4h6c5bmshvz), and [skywatch.blue](https://bsky.app/profile/skywatch.blue) 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). 6 6 ··· 13 13 - PDSs settings 14 14 - Can set the watch for multiple PDSs 15 15 - An array of email addresses to send the notifications to 16 + - An optional webhook URL to send notifications to 16 17 - On startup should it query your PDS to find all the active accounts and add them to the watch list 17 18 - Should it subscribe to your PDS to auto pick up new accounts (cursor resume does not work for this since the startup backfill can usually handle most backfills) 18 19 - 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.
+2
settings.toml.example
··· 6 6 notifyEmails = ["admin@example.com", "another@example.com"] 7 7 # Will be used for auto take downs on down the road 8 8 pdsAdminPassword = "secure" 9 + # Optional webhook URL for notifications 10 + # notifyWebhookUrl = "https://example.com/webhook" 9 11 # Loads all the historic accounts in 10 12 backfillAccounts = true 11 13 # Listens for new accounts
+46 -22
src/handlers/handleNewLabel.ts
··· 8 8 import type PQueue from "p-queue"; 9 9 import { Client, simpleFetchHandler } from "@atcute/client"; 10 10 import { ComAtprotoAdminUpdateSubjectStatus } from "@atcute/atproto"; 11 + import { sendWebhookNotification } from "../webhook.js"; 11 12 const adminAuthHeader = (password: string) => ({ 12 13 Authorization: `Basic ${Buffer.from(`admin:${password}`).toString("base64")}`, 13 14 }); ··· 120 121 // Perform action 121 122 switch (labelConfig.action) { 122 123 case "notify": 124 + const notificationParams = { 125 + did: targetDid, 126 + pds: pdsConfig.host, 127 + label: label.val, 128 + labeler: config.host, 129 + negated: label.neg ?? false, 130 + dateApplied: labledDate, 131 + targetUri: label.uri, 132 + takeDown: false, 133 + }; 134 + 123 135 await mailQueue.add(() => 124 - sendLabelNotification(pdsConfig.notifyEmails, { 125 - did: targetDid, 126 - pds: pdsConfig.host, 127 - label: label.val, 128 - labeler: config.host, 129 - negated: label.neg ?? false, 130 - dateApplied: labledDate, 131 - targetUri: label.uri, 132 - takeDown: false, 133 - }).catch((err) => 136 + sendLabelNotification(pdsConfig.notifyEmails, notificationParams).catch((err) => 134 137 logger.error({ err }, "Error sending label notification email"), 135 138 ), 136 139 ); 140 + 141 + if (pdsConfig.notifyWebhookUrl) { 142 + await mailQueue.add(() => 143 + sendWebhookNotification(pdsConfig.notifyWebhookUrl!, notificationParams).catch((err) => 144 + logger.error({ err }, "Error sending webhook notification"), 145 + ), 146 + ); 147 + } 137 148 break; 138 149 case "takedown": { 139 150 // Can be a successful takedown or not 140 - let takedownActionSucceededs: boolean; 151 + let takedownActionSucceededs: boolean | undefined; 141 152 142 153 if (pdsConfig.pdsAdminPassword) { 143 154 const rpc = new Client({ ··· 226 237 ); 227 238 } 228 239 240 + const notificationParams = { 241 + did: targetDid, 242 + pds: pdsConfig.host, 243 + label: label.val, 244 + labeler: config.host, 245 + negated: label.neg ?? false, 246 + dateApplied: labledDate, 247 + takeDown: true, 248 + targetUri: label.uri, 249 + takedownSuccess: takedownActionSucceededs, 250 + }; 251 + 229 252 await mailQueue.add(() => 230 - sendLabelNotification(pdsConfig.notifyEmails, { 231 - did: targetDid, 232 - pds: pdsConfig.host, 233 - label: label.val, 234 - labeler: config.host, 235 - negated: label.neg ?? false, 236 - dateApplied: labledDate, 237 - takeDown: true, 238 - targetUri: label.uri, 239 - takedownSuccess: takedownActionSucceededs, 240 - }).catch((err) => 253 + sendLabelNotification(pdsConfig.notifyEmails, notificationParams).catch((err) => 241 254 logger.error( 242 255 { err }, 243 256 "Error sending takedown notification email", 244 257 ), 245 258 ), 246 259 ); 260 + 261 + if (pdsConfig.notifyWebhookUrl) { 262 + await mailQueue.add(() => 263 + sendWebhookNotification(pdsConfig.notifyWebhookUrl!, notificationParams).catch((err) => 264 + logger.error( 265 + { err }, 266 + "Error sending takedown webhook notification", 267 + ), 268 + ), 269 + ); 270 + } 247 271 break; 248 272 } 249 273 }
+37 -16
src/mailer.ts
··· 15 15 const transporter = 16 16 !resendApiKey && smtpUrl ? nodemailer.createTransport(smtpUrl) : null; 17 17 18 - export const sendLabelNotification = async ( 19 - emails: string[], 20 - params: { 21 - did: string; 22 - pds: string; 23 - label: string; 24 - labeler: string; 25 - negated: boolean; 26 - dateApplied: Date; 27 - takeDown: boolean; 28 - targetUri: string; 29 - takedownSuccess?: boolean; 30 - }, 31 - ) => { 18 + export const getInfoFromParams = (params: { 19 + did: string; 20 + pds: string; 21 + label: string; 22 + labeler: string; 23 + negated: boolean; 24 + dateApplied: Date; 25 + takeDown: boolean; 26 + targetUri: string; 27 + takedownSuccess?: boolean | undefined; 28 + }): string => { 32 29 const { 33 30 did, 34 31 pds, ··· 41 38 takedownSuccess, 42 39 } = params; 43 40 44 - const subject = `Label "${label}" ${negated ? "negated" : "applied"} โ€” ${did} - ${pds}`; 45 41 let info = [ 46 42 `A label event was detected.`, 47 43 ``, ··· 74 70 } 75 71 } 76 72 77 - const text = info.join("\n"); 73 + return info.join("\n"); 74 + } 75 + 76 + export const sendLabelNotification = async ( 77 + emails: string[], 78 + params: { 79 + did: string; 80 + pds: string; 81 + label: string; 82 + labeler: string; 83 + negated: boolean; 84 + dateApplied: Date; 85 + takeDown: boolean; 86 + targetUri: string; 87 + takedownSuccess?: boolean | undefined; 88 + }, 89 + ) => { 90 + const { 91 + did, 92 + pds, 93 + label, 94 + negated, 95 + } = params; 96 + 97 + const subject = `Label "${label}" ${negated ? "negated" : "applied"} โ€” ${did} - ${pds}`; 98 + const text = getInfoFromParams(params); 78 99 79 100 if (resend) { 80 101 await resend.emails.send({
+1
src/types/settings.ts
··· 3 3 export interface PDSConfig { 4 4 host: string; 5 5 notifyEmails: string[]; 6 + notifyWebhookUrl?: string; 6 7 pdsAdminPassword?: string; 7 8 backfillAccounts: boolean; 8 9 listenForNewAccounts: boolean;
+36
src/webhook.ts
··· 1 + import { logger } from "./logger.js"; 2 + import { getInfoFromParams } from "./mailer.js"; 3 + 4 + export const sendWebhookNotification = async ( 5 + webhookUrl: string, 6 + params: { 7 + did: string; 8 + pds: string; 9 + label: string; 10 + labeler: string; 11 + negated: boolean; 12 + dateApplied: Date; 13 + takeDown: boolean; 14 + targetUri: string; 15 + takedownSuccess?: boolean | undefined; 16 + }, 17 + ) => { 18 + const text = getInfoFromParams(params); 19 + 20 + const response = await fetch(webhookUrl, { 21 + method: "POST", 22 + headers: { "Content-Type": "application/json" }, 23 + body: JSON.stringify({ 24 + ...params, 25 + content: text, 26 + dateApplied: params.dateApplied.toISOString(), 27 + }), 28 + }); 29 + 30 + if (!response.ok) { 31 + logger.error( 32 + { status: response.status, webhookUrl }, 33 + "Webhook notification failed", 34 + ); 35 + } 36 + };

History

1 round 1 comment
sign up or login to add to the discussion
4 commits
expand
feat: add support for (optional) webhook notifications
type-fix: assign default value for bool
doc update for webhook mentions
fix: rework typing
expand 1 comment

LGTM! Thank you so much!

pull request successfully merged