PDS Admin tool make it easier to moderate your PDS with labels
43
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 + };