Files
modstagram-back/utils/pushNotification.js
2026-09-24 21:03:27 +03:30

129 lines
6.3 KiB
JavaScript
Raw Permalink 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
}
}
/**
* ارسال پوش به یه لیست توکن مشخص — پایه‌ی مسیرهای دسته‌جمعی (فالوورها، پیام‌های همگانی پنل)
* dataOnly: اگه true باشه، هیچ بلاک notification فرستاده نمی‌شه — فقط data (رشته‌ای‌شده).
* دلیلش: react-native-firebase وقتی پیام یه notification داشته باشه، خودِ سیستم‌عامل
* (نه کدِ اپ) پیش‌فرض نمایشش می‌ده و setBackgroundMessageHandler برای اون پیام اصلاً صدا
* زده نمی‌شه — یعنی هیچ‌وقت نمی‌شه آیکونِ بزرگ (آواتار) رو ازش کنترل کرد. فقط برای
* توکن‌های موبایل (sendPushToUser) استفاده می‌شه؛ وب دست‌نخورده می‌مونه (already working)
*/
async function sendPushToTokens (tokens, { title, description, type, data, image, dataOnly } = {}) {
const app = getFirebaseApp()
const cleanTokens = (tokens || []).filter(Boolean)
if (!app || cleanTokens.length === 0) return { successCount: 0, invalidTokens: [] }
const dataPayload = { type, ...data }
if (dataOnly) {
// notifee (index.js پس‌زمینه/foreground) از همین فیلدها نوتیف غنی می‌سازه
dataPayload.title = title || ''
dataPayload.description = description || ''
if (image) dataPayload.image = image
}
const message = {
// notification.image: عکس/آواتار نمایش داده‌شده توی خودِ نوتیف (وب/آی‌اواس، و اندرویدِ
// dataOnly:false) — عین رفتار واتساپ/اینستاگرام برای نوتیف پیام
...(dataOnly ? {} : { notification: { title, body: description, ...(image ? { image } : {}) } }),
data: Object.fromEntries(
Object.entries(dataPayload).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 }
}
/**
* ارسال پوش به یک کاربر مشخص — توکن‌هاش رو از دیتابیس می‌خونه و توکن‌های نامعتبر رو پاک می‌کنه.
* موبایل و وب جدا ارسال می‌شن: موبایل dataOnly (نوتیفِ غنی رو notifee توی index.js/App
* می‌سازه)، وب عین قبل با بلاک notification (رفتارِ فعلاً درستِ وب دست‌نخورده می‌مونه)
*/
async function sendPushToUser (userId, { title, description, type, data, image } = {}) {
try {
const user = await UserModel.findById(userId).select('fcm_tokens fcm_web_tokens').lean()
const mobileTokens = user?.fcm_tokens || []
const webTokens = user?.fcm_web_tokens || []
if (mobileTokens.length === 0 && webTokens.length === 0) return
const [mobileResult, webResult] = await Promise.all([
mobileTokens.length
? sendPushToTokens(mobileTokens, { title, description, type, data, image, dataOnly: true })
: Promise.resolve({ invalidTokens: [] }),
webTokens.length
? sendPushToTokens(webTokens, { title, description, type, data, image })
: Promise.resolve({ invalidTokens: [] })
])
if (mobileResult.invalidTokens.length > 0 || webResult.invalidTokens.length > 0) {
await UserModel.updateOne(
{ _id: userId },
{
$pull: {
fcm_tokens: { $in: mobileResult.invalidTokens },
fcm_web_tokens: { $in: webResult.invalidTokens }
}
}
)
}
} catch (error) {
console.error('[pushNotification] sendPushToUser خطا:', error.message)
}
}
module.exports = { sendPushToUser, sendPushToTokens, getFirebaseApp }