This commit is contained in:
payacom
2026-08-31 20:45:41 +03:30
parent fa9f23c61d
commit 3f416a7a60
367 changed files with 621895 additions and 6443 deletions

100
utils/pushNotification.js Normal file
View File

@@ -0,0 +1,100 @@
const admin = require('firebase-admin')
// firebase-admin v12+ دیگه admin.messaging(app) رو صادر نمی‌کنه — باید از ساب‌ماژول جدا بگیریم
const { getMessaging } = require('firebase-admin/messaging')
const UserModel = require('../models/UserModel')
let firebaseApp = null
let warnedMissingCredential = false
function getFirebaseApp () {
if (firebaseApp) return firebaseApp
const raw = process.env.FIREBASE_SERVICE_ACCOUNT_BASE64
if (!raw) {
// بدون این هشدار، ارسال پوش کاملاً بی‌صدا هیچ‌کاری نمی‌کنه (نوتیف داخل‌برنامه‌ای و
// سوکت درست کار می‌کنن، فقط پوش واقعاً نمی‌ره) و تشخیصش خیلی سخت می‌شه — یک‌بار در طول
// عمر پروسه لاگ می‌کنیم تا توی لاگ سرور واضح دیده بشه
if (!warnedMissingCredential) {
warnedMissingCredential = true
console.error('[pushNotification] FIREBASE_SERVICE_ACCOUNT_BASE64 تنظیم نشده — ارسال پوش نوتیفیکیشن غیرفعاله (نوتیف داخل‌برنامه‌ای همچنان کار می‌کنه)')
}
return null
}
try {
const serviceAccount = JSON.parse(Buffer.from(raw, 'base64').toString('utf8'))
// firebase-admin v12+ آبجکت credential رو دیگه تو‌درتو صادر نمی‌کنه (admin.cert مستقیم)؛
// نسخه‌های قدیمی‌تر هنوز admin.credential.cert دارن — هر دو رو پشتیبانی می‌کنیم
const cert = admin.credential?.cert ?? admin.cert
firebaseApp = admin.initializeApp({
credential: cert(serviceAccount)
})
return firebaseApp
} catch (error) {
console.error('[pushNotification] پیکربندی FIREBASE_SERVICE_ACCOUNT_BASE64 نامعتبر است:', error.message)
return null
}
}
/** ارسال پوش به یه لیست توکن مشخص — پایه‌ی مسیرهای دسته‌جمعی (فالوورها، پیام‌های همگانی پنل) */
async function sendPushToTokens (tokens, { title, description, type, data, image } = {}) {
const app = getFirebaseApp()
const cleanTokens = (tokens || []).filter(Boolean)
if (!app || cleanTokens.length === 0) return { successCount: 0, invalidTokens: [] }
const message = {
// notification.image: عکس/آواتار نمایش داده‌شده توی خودِ نوتیف (اندروید: بزرگ‌آیکون/
// big-picture، iOS: پیوست) — عین رفتار واتساپ/اینستاگرام برای نوتیف پیام
notification: { title, body: description, ...(image ? { image } : {}) },
data: Object.fromEntries(
Object.entries({ type, ...data }).map(([k, v]) => [k, String(v ?? '')])
),
tokens: cleanTokens
}
const invalidTokens = []
let successCount = 0
// FCM حداکثر ۵۰۰ توکن در هر multicast قبول می‌کنه
for (let i = 0; i < cleanTokens.length; i += 500) {
const chunk = cleanTokens.slice(i, i + 500)
try {
const messaging = admin.messaging ? admin.messaging(app) : getMessaging(app)
const res = await messaging.sendEachForMulticast({ ...message, tokens: chunk })
successCount += res.successCount
res.responses.forEach((r, idx) => {
if (!r.success && r.error?.code === 'messaging/registration-token-not-registered') {
invalidTokens.push(chunk[idx])
}
})
} catch (error) {
console.error('[pushNotification] خطا در ارسال دسته‌ای:', error.message)
}
}
return { successCount, invalidTokens }
}
/** ارسال پوش به یک کاربر مشخص — توکن‌هاش رو از دیتابیس می‌خونه و توکن‌های نامعتبر رو پاک می‌کنه */
async function sendPushToUser (userId, { title, description, type, data, image } = {}) {
try {
const user = await UserModel.findById(userId).select('fcm_tokens fcm_web_tokens').lean()
const tokens = [...(user?.fcm_tokens || []), ...(user?.fcm_web_tokens || [])]
if (tokens.length === 0) return
const { invalidTokens } = await sendPushToTokens(tokens, { title, description, type, data, image })
if (invalidTokens.length > 0) {
await UserModel.updateOne(
{ _id: userId },
{
$pull: {
fcm_tokens: { $in: invalidTokens },
fcm_web_tokens: { $in: invalidTokens }
}
}
)
}
} catch (error) {
console.error('[pushNotification] sendPushToUser خطا:', error.message)
}
}
module.exports = { sendPushToUser, sendPushToTokens, getFirebaseApp }