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.

resend api so it works on railway

+93 -17
+2
.env.example
··· 1 1 DATABASE_URL=file:./label-watcher.db 2 2 MIGRATIONS_FOLDER=drizzle 3 + # Email sending: set RESEND_API_KEY to use Resend, or NOTIFY_SMTP_URL to use SMTP (one is required) 4 + RESEND_API_KEYb=123 3 5 NOTIFY_SMTP_URL=smtp://localhost:1025 4 6 NOTIFY_SENDER_EMAIL=yougotmail@pdsmoover.com 5 7 LOG_LEVEL=info
+2
Dockerfile
··· 20 20 21 21 COPY --from=builder /app/dist /app/dist 22 22 COPY ./drizzle /app/drizzle 23 + # A very bad hack. need to see how to get a toml file to the volume of railway without this 24 + # COPY settings.toml /app/settings.toml 23 25 COPY --from=builder /app/package.json /app/pnpm-lock.yaml /app/pnpm-workspace.yaml ./ 24 26 # RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod --frozen-lockfile 25 27 RUN pnpm install --prod --frozen-lockfile
+1
package.json
··· 23 23 "nodemailer": "^8.0.1", 24 24 "p-queue": "^9.1.0", 25 25 "pino": "^10.3.1", 26 + "resend": "^6.9.2", 26 27 "smol-toml": "^1.6.0" 27 28 }, 28 29 "devDependencies": {
+54
pnpm-lock.yaml
··· 32 32 pino: 33 33 specifier: ^10.3.1 34 34 version: 10.3.1 35 + resend: 36 + specifier: ^6.9.2 37 + version: 6.9.2 35 38 smol-toml: 36 39 specifier: ^1.6.0 37 40 version: 1.6.0 ··· 456 459 '@pinojs/redact@0.4.0': 457 460 resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} 458 461 462 + '@stablelib/base64@1.0.1': 463 + resolution: {integrity: sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==} 464 + 459 465 '@standard-schema/spec@1.1.0': 460 466 resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} 461 467 ··· 630 636 fast-safe-stringify@2.1.1: 631 637 resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} 632 638 639 + fast-sha256@1.3.0: 640 + resolution: {integrity: sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==} 641 + 633 642 fetch-blob@3.2.0: 634 643 resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} 635 644 engines: {node: ^12.20 || >= 14.13} ··· 754 763 resolution: {integrity: sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==} 755 764 hasBin: true 756 765 766 + postal-mime@2.7.3: 767 + resolution: {integrity: sha512-MjhXadAJaWgYzevi46+3kLak8y6gbg0ku14O1gO/LNOuay8dO+1PtcSGvAdgDR0DoIsSaiIA8y/Ddw6MnrO0Tw==} 768 + 757 769 postgres-array@2.0.0: 758 770 resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} 759 771 engines: {node: '>=4'} ··· 786 798 resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} 787 799 engines: {node: '>= 12.13.0'} 788 800 801 + resend@6.9.2: 802 + resolution: {integrity: sha512-uIM6CQ08tS+hTCRuKBFbOBvHIGaEhqZe8s4FOgqsVXSbQLAhmNWpmUhG3UAtRnmcwTWFUqnHa/+Vux8YGPyDBA==} 803 + engines: {node: '>=20'} 804 + peerDependencies: 805 + '@react-email/render': '*' 806 + peerDependenciesMeta: 807 + '@react-email/render': 808 + optional: true 809 + 789 810 resolve-pkg-maps@1.0.0: 790 811 resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} 791 812 ··· 814 835 resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} 815 836 engines: {node: '>= 10.x'} 816 837 838 + standardwebhooks@1.0.0: 839 + resolution: {integrity: sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==} 840 + 817 841 strip-json-comments@5.0.3: 818 842 resolution: {integrity: sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==} 819 843 engines: {node: '>=14.16'} 844 + 845 + svix@1.84.1: 846 + resolution: {integrity: sha512-K8DPPSZaW/XqXiz1kEyzSHYgmGLnhB43nQCMeKjWGCUpLIpAMMM8kx3rVVOSm6Bo6EHyK1RQLPT4R06skM/MlQ==} 820 847 821 848 thread-stream@4.0.0: 822 849 resolution: {integrity: sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA==} ··· 840 867 unicode-segmenter@0.14.5: 841 868 resolution: {integrity: sha512-jHGmj2LUuqDcX3hqY12Ql+uhUTn8huuxNZGq7GvtF6bSybzH3aFgedYu/KTzQStEgt1Ra2F3HxadNXsNjb3m3g==} 842 869 870 + uuid@10.0.0: 871 + resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==} 872 + hasBin: true 873 + 843 874 web-streams-polyfill@3.3.3: 844 875 resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} 845 876 engines: {node: '>= 8'} ··· 1157 1188 1158 1189 '@pinojs/redact@0.4.0': {} 1159 1190 1191 + '@stablelib/base64@1.0.1': {} 1192 + 1160 1193 '@standard-schema/spec@1.1.0': {} 1161 1194 1162 1195 '@types/node@25.3.0': ··· 1282 1315 fast-copy@4.0.2: {} 1283 1316 1284 1317 fast-safe-stringify@2.1.1: {} 1318 + 1319 + fast-sha256@1.3.0: {} 1285 1320 1286 1321 fetch-blob@3.2.0: 1287 1322 dependencies: ··· 1433 1468 sonic-boom: 4.2.1 1434 1469 thread-stream: 4.0.0 1435 1470 1471 + postal-mime@2.7.3: {} 1472 + 1436 1473 postgres-array@2.0.0: 1437 1474 optional: true 1438 1475 ··· 1460 1497 1461 1498 real-require@0.2.0: {} 1462 1499 1500 + resend@6.9.2: 1501 + dependencies: 1502 + postal-mime: 2.7.3 1503 + svix: 1.84.1 1504 + 1463 1505 resolve-pkg-maps@1.0.0: {} 1464 1506 1465 1507 safe-stable-stringify@2.5.0: {} ··· 1481 1523 1482 1524 split2@4.2.0: {} 1483 1525 1526 + standardwebhooks@1.0.0: 1527 + dependencies: 1528 + '@stablelib/base64': 1.0.1 1529 + fast-sha256: 1.3.0 1530 + 1484 1531 strip-json-comments@5.0.3: {} 1485 1532 1533 + svix@1.84.1: 1534 + dependencies: 1535 + standardwebhooks: 1.0.0 1536 + uuid: 10.0.0 1537 + 1486 1538 thread-stream@4.0.0: 1487 1539 dependencies: 1488 1540 real-require: 0.2.0 ··· 1496 1548 undici-types@7.18.2: {} 1497 1549 1498 1550 unicode-segmenter@0.14.5: {} 1551 + 1552 + uuid@10.0.0: {} 1499 1553 1500 1554 web-streams-polyfill@3.3.3: {} 1501 1555
+34 -17
src/mailer.ts
··· 1 1 import nodemailer from "nodemailer"; 2 + import { Resend } from "resend"; 2 3 4 + const resendApiKey = process.env.RESEND_API_KEY; 3 5 const smtpUrl = process.env.NOTIFY_SMTP_URL; 4 6 const senderEmail = process.env.NOTIFY_SENDER_EMAIL; 5 7 6 - if (!smtpUrl) throw new Error("NOTIFY_SMTP_URL is not set"); 8 + if (!resendApiKey && !smtpUrl) { 9 + throw new Error("Either RESEND_API_KEY or NOTIFY_SMTP_URL must be set"); 10 + } 7 11 if (!senderEmail) throw new Error("NOTIFY_SENDER_EMAIL is not set"); 8 12 9 - const transporter = nodemailer.createTransport(smtpUrl); 13 + const resend = resendApiKey ? new Resend(resendApiKey) : null; 14 + const transporter = !resendApiKey && smtpUrl ? nodemailer.createTransport(smtpUrl) : null; 10 15 11 16 export const sendLabelNotification = async ( 12 17 emails: string[], ··· 21 26 ) => { 22 27 const { did, pds, label, labeler, negated, dateApplied } = params; 23 28 24 - await transporter.sendMail({ 25 - from: senderEmail, 26 - to: emails.join(", "), 27 - subject: `Label "${label}" ${negated ? "negated" : "applied"} — ${did} - ${pds}`, 28 - text: [ 29 - `A label event was detected.`, 30 - ``, 31 - `DID: ${did}`, 32 - `PDS: ${pds}`, 33 - `Label: ${label}`, 34 - `Labeler: ${labeler}`, 35 - `Negated: ${negated}`, 36 - `Date: ${dateApplied.toISOString()}`, 37 - ].join("\n"), 38 - }); 29 + const subject = `Label "${label}" ${negated ? "negated" : "applied"} — ${did} - ${pds}`; 30 + const text = [ 31 + `A label event was detected.`, 32 + ``, 33 + `DID: ${did}`, 34 + `PDS: ${pds}`, 35 + `Label: ${label}`, 36 + `Labeler: ${labeler}`, 37 + `Negated: ${negated}`, 38 + `Date: ${dateApplied.toISOString()}`, 39 + ].join("\n"); 40 + 41 + if (resend) { 42 + await resend.emails.send({ 43 + from: senderEmail, 44 + to: emails, 45 + subject, 46 + text, 47 + }); 48 + } else { 49 + await transporter!.sendMail({ 50 + from: senderEmail, 51 + to: emails.join(", "), 52 + subject, 53 + text, 54 + }); 55 + } 39 56 };