forked from Prompt-Hash-Stellar/prompt-hash
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathemailNotifications.ts
More file actions
154 lines (135 loc) · 5.94 KB
/
Copy pathemailNotifications.ts
File metadata and controls
154 lines (135 loc) · 5.94 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
/**
* emailNotifications.ts — Issue #112
*
* Email notification service for PromptPurchased and PromptUpdated events.
* Uses nodemailer with any SMTP provider (SendGrid, Postmark, SES, etc.).
* Users opt-in/out per notification type via User model preferences.
*
* Configuration (env vars):
* EMAIL_SMTP_HOST, EMAIL_SMTP_PORT, EMAIL_SMTP_USER, EMAIL_SMTP_PASS
* EMAIL_FROM_ADDRESS (e.g. "PromptHash <noreply@prompthash.io>")
*/
import nodemailer from "nodemailer";
import User from "../models/User.js";
// ── Types ──────────────────────────────────────────────────────────────────────
export type NotificationEvent = "PromptPurchased" | "PromptUpdated";
export interface PurchasePayload {
buyerWallet: string;
promptTitle: string;
promptId: string;
txHash?: string;
}
export interface UpdatePayload {
ownerWallet: string;
promptTitle: string;
promptId: string;
versionIndex: number;
}
// ── Transport ─────────────────────────────────────────────────────────────────
function createTransport() {
return nodemailer.createTransport({
host: process.env.EMAIL_SMTP_HOST,
port: Number(process.env.EMAIL_SMTP_PORT ?? 587),
secure: process.env.EMAIL_SMTP_PORT === "465",
auth: {
user: process.env.EMAIL_SMTP_USER,
pass: process.env.EMAIL_SMTP_PASS,
},
});
}
const FROM = process.env.EMAIL_FROM_ADDRESS ?? "PromptHash <noreply@prompthash.io>";
// ── Template builders ─────────────────────────────────────────────────────────
function buildPurchaseEmail(payload: PurchasePayload): { subject: string; html: string } {
return {
subject: `🎉 Your prompt "${payload.promptTitle}" was purchased`,
html: `
<h2>Congratulations!</h2>
<p>A buyer (<code>${payload.buyerWallet.slice(0, 8)}…</code>) just purchased
your prompt <strong>${payload.promptTitle}</strong>.</p>
${payload.txHash ? `<p>Transaction: <code>${payload.txHash}</code></p>` : ""}
<p><a href="${process.env.APP_URL ?? "https://prompthash.io"}/prompts/${payload.promptId}">
View prompt
</a></p>
<hr/>
<small>To manage your notification preferences visit your account settings.</small>
`,
};
}
function buildUpdateEmail(payload: UpdatePayload): { subject: string; html: string } {
return {
subject: `📦 Prompt updated: "${payload.promptTitle}" (v${payload.versionIndex + 1})`,
html: `
<h2>Prompt Updated</h2>
<p>The prompt <strong>${payload.promptTitle}</strong> you purchased has been updated
to version ${payload.versionIndex + 1}.</p>
<p><a href="${process.env.APP_URL ?? "https://prompthash.io"}/prompts/${payload.promptId}">
View updated prompt
</a></p>
<hr/>
<small>To manage your notification preferences visit your account settings.</small>
`,
};
}
// ── Core send helper ──────────────────────────────────────────────────────────
async function sendEmail(to: string, subject: string, html: string): Promise<void> {
if (!process.env.EMAIL_SMTP_HOST) {
console.warn("[email] SMTP not configured — skipping email to", to);
return;
}
const transport = createTransport();
await transport.sendMail({ from: FROM, to, subject, html });
console.log(`[email] Sent "${subject}" to ${to}`);
}
// ── User preference helpers ───────────────────────────────────────────────────
async function getEmailForWallet(wallet: string): Promise<string | null> {
const user = await User.findOne({ walletAddress: wallet.toLowerCase() }).lean();
return (user as { email?: string } | null)?.email ?? null;
}
async function hasOptedIn(wallet: string, event: NotificationEvent): Promise<boolean> {
const user = await User.findOne({ walletAddress: wallet.toLowerCase() }).lean();
if (!user) return false;
const prefs = (user as { notificationPreferences?: Partial<Record<NotificationEvent, boolean>> })
.notificationPreferences;
// Default opt-in when prefs not explicitly set
return prefs?.[event] !== false;
}
// ── Public API ────────────────────────────────────────────────────────────────
/**
* Notify a prompt creator when their prompt is purchased.
* Looks up the creator wallet from the User collection.
*/
export async function notifyPromptPurchased(
creatorWallet: string,
payload: PurchasePayload
): Promise<void> {
try {
if (!(await hasOptedIn(creatorWallet, "PromptPurchased"))) return;
const email = await getEmailForWallet(creatorWallet);
if (!email) return;
const { subject, html } = buildPurchaseEmail(payload);
await sendEmail(email, subject, html);
} catch (err) {
console.error("[email] notifyPromptPurchased failed:", err);
}
}
/**
* Notify buyers of a prompt that a new version has been published.
*/
export async function notifyPromptUpdated(
buyerWallets: string[],
payload: UpdatePayload
): Promise<void> {
const { subject, html } = buildUpdateEmail(payload);
await Promise.allSettled(
buyerWallets.map(async (wallet) => {
try {
if (!(await hasOptedIn(wallet, "PromptUpdated"))) return;
const email = await getEmailForWallet(wallet);
if (!email) return;
await sendEmail(email, subject, html);
} catch (err) {
console.error(`[email] notifyPromptUpdated failed for ${wallet}:`, err);
}
})
);
}