101 lines
4.6 KiB
JavaScript
101 lines
4.6 KiB
JavaScript
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 }
|