Files
modstagram-back/utils/pushNotification.js
payacom 3f416a7a60 full
2026-08-31 20:45:41 +03:30

101 lines
4.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 }