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

feat: add support for (optional) webhook notifications

authored by andresitorresm.com and committed by tangled.org 5b12a4cc 9a9f57e4

+121 -37
+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
+45 -21
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 ··· 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; 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; 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; 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 + };