From aab762a73a9de6633e2a66be3843ae615257baff Mon Sep 17 00:00:00 2001 From: arsam Date: Fri, 18 Sep 2026 23:19:07 +0330 Subject: [PATCH] Import source: app/backend/scripts/weekly-digest.mjs Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality. --- app/backend/scripts/weekly-digest.mjs | 127 ++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 app/backend/scripts/weekly-digest.mjs diff --git a/app/backend/scripts/weekly-digest.mjs b/app/backend/scripts/weekly-digest.mjs new file mode 100644 index 0000000..d7121c2 --- /dev/null +++ b/app/backend/scripts/weekly-digest.mjs @@ -0,0 +1,127 @@ +import { PrismaClient } from '@prisma/client'; + +const prisma = new PrismaClient(); +const SITE_URL = (process.env.TEKNAV_SITE_URL || 'https://www.teknav.ir').replace(/\/$/, ''); + +function esc(value) { + return String(value ?? '') + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + +function strip(value) { + return String(value ?? '').replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim(); +} + +function weekSlug(date = new Date()) { + const day = date.toISOString().slice(0, 10); + return `weekly-digest-${day}`; +} + +async function sendEmail({ to, subject, html }) { + if (!process.env.SMTP_HOST) { + console.info('[weekly-digest:stub] Would send to', to, '|', subject); + return; + } + const nodemailer = await import('nodemailer'); + const port = Number(process.env.SMTP_PORT ?? 587); + const transport = nodemailer.createTransport({ + host: process.env.SMTP_HOST, + port, + secure: port === 465, + auth: process.env.SMTP_USER ? { user: process.env.SMTP_USER, pass: process.env.SMTP_PASS } : undefined, + }); + await transport.sendMail({ + from: process.env.SMTP_FROM ?? 'no-reply@teknav.ir', + to, + subject, + html, + }); +} + +function digestHtml(articles, unsubscribeToken = '') { + const unsubscribe = `${SITE_URL}/api/newsletter/unsubscribe?token=${unsubscribeToken}`; + const cards = articles.map((article) => { + const url = `${SITE_URL}${article.canonicalPath || `/article/${article.slug}`}`; + return ` +
+
${esc(article.category.name)}
+

${esc(article.title)}

+

${esc(strip(article.metaDescription || article.summary).slice(0, 180))}

+ خواندن مقاله +
`; + }).join(''); + return ` +
+
+
گزیده هفتگی تکنّاو
+

مقاله‌های مهم هفته

+

منتخبی از تازه‌ترین تحلیل‌های فناوری، داده، امنیت، نرم‌افزار و سخت‌افزار.

+ ${cards} +

برای لغو عضویت از خبرنامه، روی این لینک بزنید: ${esc(unsubscribe)}

+
+
`; +} + +try { + const now = new Date(); + const since = new Date(now); + since.setDate(since.getDate() - 7); + const slug = process.env.TEKNAV_DIGEST_SLUG || weekSlug(now); + const existing = await prisma.newsletterCampaign.findUnique({ where: { slug } }); + if (existing?.sentAt) { + console.info(`[weekly-digest] ${slug} already sent`); + process.exit(0); + } + + const [articles, subscribers, creator] = await Promise.all([ + prisma.article.findMany({ + where: { publishedAt: { gte: since, lte: now } }, + include: { category: true, author: true }, + orderBy: [{ featured: 'desc' }, { publishedAt: 'desc' }], + take: 12, + }), + prisma.newsletterSubscriber.findMany({ orderBy: { createdAt: 'asc' } }), + prisma.user.findFirst({ where: { role: { in: ['admin', 'editor'] } }, orderBy: { createdAt: 'asc' } }), + ]); + + if (!creator) throw new Error('No admin/editor user exists for newsletter campaign ownership.'); + if (articles.length === 0) { + console.info('[weekly-digest] no articles published in the last 7 days'); + process.exit(0); + } + + const subject = `گزیده هفتگی تکنّاو - ${now.toLocaleDateString('fa-IR')}`; + const campaign = existing ?? await prisma.newsletterCampaign.create({ + data: { + subject, + slug, + bodyHtml: digestHtml(articles), + createdById: creator.id, + }, + }); + + let sent = 0; + for (const subscriber of subscribers) { + await sendEmail({ + to: subscriber.email, + subject: campaign.subject, + html: digestHtml(articles, subscriber.unsubscribeToken), + }); + sent += 1; + } + + await prisma.newsletterCampaign.update({ + where: { id: campaign.id }, + data: { sentAt: new Date(), recipientCount: sent, bodyHtml: digestHtml(articles) }, + }); + await prisma.activityLog.create({ + data: { action: 'خبرنامه هفتگی ارسال شد', target: campaign.subject, type: 'newsletter', userId: creator.id }, + }).catch(() => undefined); + console.info(`[weekly-digest] sent ${sent} email(s) for ${slug}`); +} finally { + await prisma.$disconnect(); +}