WIP: PDS Admin tool to hopefully make it easier to moderate your PDS
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

you got mail

+85 -3
+1 -1
.env
··· 1 1 DATABASE_URL=file:./label-watcher.db 2 2 MIGRATIONS_FOLDER=drizzle 3 - NOTIFY_SMTP_URL=smtps://resend:.... 3 + NOTIFY_SMTP_URL=smtp://localhost:1025 4 4 NOTIFY_SENDER_EMAIL=yougotmail@pdsmoover.com 5 5 LOG_LEVEL=debug
+1
.env.example
··· 2 2 MIGRATIONS_FOLDER=drizzle 3 3 NOTIFY_SMTP_URL=smtps://resend:.... 4 4 NOTIFY_SENDER_EMAIL=yougotmail@pdsmoover.com 5 + NOTIFY_RECIPIENT_EMAILS=you@example.com,other@example.com 5 6 LOG_LEVEL=info
+2
package.json
··· 20 20 "@atcute/firehose": "^0.1.0", 21 21 "@libsql/client": "^0.17.0", 22 22 "drizzle-orm": "^0.45.1", 23 + "nodemailer": "^8.0.1", 23 24 "p-queue": "^9.1.0", 24 25 "pino": "^10.3.1", 25 26 "smol-toml": "^1.6.0" 26 27 }, 27 28 "devDependencies": { 28 29 "@types/node": "^25.3.0", 30 + "@types/nodemailer": "^7.0.11", 29 31 "drizzle-kit": "^0.31.9", 30 32 "pino-pretty": "^13.1.3", 31 33 "typescript": "^5.9.3"
+19
pnpm-lock.yaml
··· 23 23 drizzle-orm: 24 24 specifier: ^0.45.1 25 25 version: 0.45.1(@libsql/client@0.17.0)(kysely@0.22.0)(pg@8.18.0) 26 + nodemailer: 27 + specifier: ^8.0.1 28 + version: 8.0.1 26 29 p-queue: 27 30 specifier: ^9.1.0 28 31 version: 9.1.0 ··· 36 39 '@types/node': 37 40 specifier: ^25.3.0 38 41 version: 25.3.0 42 + '@types/nodemailer': 43 + specifier: ^7.0.11 44 + version: 7.0.11 39 45 drizzle-kit: 40 46 specifier: ^0.31.9 41 47 version: 0.31.9 ··· 456 462 '@types/node@25.3.0': 457 463 resolution: {integrity: sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A==} 458 464 465 + '@types/nodemailer@7.0.11': 466 + resolution: {integrity: sha512-E+U4RzR2dKrx+u3N4DlsmLaDC6mMZOM/TPROxA0UAPiTgI0y4CEFBmZE+coGWTjakDriRsXG368lNk1u9Q0a2g==} 467 + 459 468 '@types/ws@8.18.1': 460 469 resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} 461 470 ··· 675 684 resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} 676 685 engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} 677 686 687 + nodemailer@8.0.1: 688 + resolution: {integrity: sha512-5kcldIXmaEjZcHR6F28IKGSgpmZHaF1IXLWFTG+Xh3S+Cce4MiakLtWY+PlBU69fLbRa8HlaGIrC/QolUpHkhg==} 689 + engines: {node: '>=6.0.0'} 690 + 678 691 on-exit-leak-free@2.1.2: 679 692 resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} 680 693 engines: {node: '>=14.0.0'} ··· 1150 1163 dependencies: 1151 1164 undici-types: 7.18.2 1152 1165 1166 + '@types/nodemailer@7.0.11': 1167 + dependencies: 1168 + '@types/node': 25.3.0 1169 + 1153 1170 '@types/ws@8.18.1': 1154 1171 dependencies: 1155 1172 '@types/node': 25.3.0 ··· 1318 1335 data-uri-to-buffer: 4.0.1 1319 1336 fetch-blob: 3.2.0 1320 1337 formdata-polyfill: 4.0.10 1338 + 1339 + nodemailer@8.0.1: {} 1321 1340 1322 1341 on-exit-leak-free@2.1.2: {} 1323 1342
+25 -2
src/handlers/handleNewLabel.ts
··· 1 1 import type { Label } from "@atcute/atproto/types/label/defs"; 2 - import type { LabelerConfig } from "../types/settings.js"; 2 + import type { LabelerConfig, PDSConfig } from "../types/settings.js"; 3 3 import { logger } from "../logger.js"; 4 + import { sendLabelNotification } from "../mailer.js"; 4 5 import type { LibSQLDatabase } from "drizzle-orm/libsql"; 5 6 import * as schema from "../db/schema.js"; 6 - import { and, count, eq } from "drizzle-orm"; 7 + import { and, eq } from "drizzle-orm"; 7 8 8 9 export const handleNewLabel = async ( 9 10 config: LabelerConfig, 10 11 label: Label, 11 12 db: LibSQLDatabase<typeof schema>, 13 + pdsConfigs: Record<string, PDSConfig>, 12 14 ) => { 13 15 try { 14 16 // TODO: MAKE SURE TO CHECK NEG ··· 33 35 .limit(1); 34 36 35 37 if (isRepoWatched.length > 0) { 38 + const watchedRepo = isRepoWatched[0]; 39 + if (watchedRepo == undefined) { 40 + throw new Error(`Unexpected error on watched repo: ${label.uri}`); 41 + } 42 + const pdsConfig = pdsConfigs[watchedRepo.pdsHost]; 43 + if (pdsConfig == undefined) { 44 + throw new Error(`Watched repo: ${watchedRepo.did} config not found`); 45 + } 46 + 36 47 logger.info( 37 48 { action: labelConfig.action }, 38 49 `Listed label: ${label.val} found. Performing the action against: ${label.uri}`, ··· 75 86 }); 76 87 } 77 88 89 + // Perform action 90 + if (labelConfig.action === "notify") { 91 + await sendLabelNotification(pdsConfig.emails, { 92 + did: label.uri, 93 + label: label.val, 94 + labeler: config.host, 95 + negated: label.neg ?? false, 96 + dateApplied: labledDate, 97 + }); 98 + } 99 + 78 100 return; 79 101 } 102 + 80 103 logger.warn( 81 104 { action: labelConfig.action }, 82 105 "Listed label found but repo is not watched. Skipping",
+37
src/mailer.ts
··· 1 + import nodemailer from "nodemailer"; 2 + 3 + const smtpUrl = process.env.NOTIFY_SMTP_URL; 4 + const senderEmail = process.env.NOTIFY_SENDER_EMAIL; 5 + 6 + if (!smtpUrl) throw new Error("NOTIFY_SMTP_URL is not set"); 7 + if (!senderEmail) throw new Error("NOTIFY_SENDER_EMAIL is not set"); 8 + 9 + const transporter = nodemailer.createTransport(smtpUrl); 10 + 11 + export const sendLabelNotification = async ( 12 + emails: string[], 13 + params: { 14 + did: string; 15 + label: string; 16 + labeler: string; 17 + negated: boolean; 18 + dateApplied: Date; 19 + }, 20 + ) => { 21 + const { did, label, labeler, negated, dateApplied } = params; 22 + 23 + await transporter.sendMail({ 24 + from: senderEmail, 25 + to: emails.join(", "), 26 + subject: `Label "${label}" ${negated ? "negated" : "applied"} — ${did}`, 27 + text: [ 28 + `A label event was detected.`, 29 + ``, 30 + `DID: ${did}`, 31 + `Label: ${label}`, 32 + `Labeler: ${labeler}`, 33 + `Negated: ${negated}`, 34 + `Date: ${dateApplied.toISOString()}`, 35 + ].join("\n"), 36 + }); 37 + };