import "dotenv/config"; import { initializeApp, cert, App, ServiceAccount } from "firebase-admin/app"; import { getMessaging, Message } from "firebase-admin/messaging"; import path from "path"; // --- KONFIGURASI --- const CONFIG = { /** * Konten notifikasi default yang akan dikirim. */ notificationPayload: { title: "Sistem Desa Mandiri", body: "Ada informasi baru untuk Anda, silakan periksa aplikasi.", }, /** * Pengaturan untuk mekanisme coba lagi (retry) jika terjadi kegagalan jaringan. */ retry: { maxRetries: 3, // Jumlah maksimal percobaan ulang delay: 2000, // Waktu tunda antar percobaan dalam milidetik (ms) }, }; // --- AKHIR KONFIGURASI --- /** * Membangun objek service account dari variabel lingkungan. */ function getServiceAccount(): ServiceAccount { const privateKey = process.env.GOOGLE_PRIVATE_KEY?.replace(/\n/g, '\n'); if (!process.env.GOOGLE_PROJECT_ID || !process.env.GOOGLE_CLIENT_EMAIL || !privateKey) { console.error("KRITIS: Variabel lingkungan Firebase (PROJECT_ID, CLIENT_EMAIL, PRIVATE_KEY) tidak lengkap."); process.exit(1); } return { projectId: process.env.GOOGLE_PROJECT_ID, clientEmail: process.env.GOOGLE_CLIENT_EMAIL, privateKey, }; } /** * Inisialisasi Firebase Admin SDK. * Hanya akan menginisialisasi satu kali. */ let firebaseApp: App | null = null; function initializeFirebase(): App { if (firebaseApp) { return firebaseApp; } try { const serviceAccount = getServiceAccount(); firebaseApp = initializeApp({ credential: cert(serviceAccount), }); console.log("Firebase Admin SDK berhasil diinisialisasi."); return firebaseApp; } catch (error: any) { console.error("KRITIS: Gagal inisialisasi Firebase. Pastikan variabel lingkungan sudah benar."); console.error(`Detail Error: ${error.message}`); process.exit(1); // Keluar dari proses jika Firebase gagal diinisialisasi } } /** * Mengambil daftar token perangkat dari database. * TODO: Ganti fungsi ini dengan logika untuk mengambil token dari database Anda. * @returns {Promise} Daftar token FCM. */ async function getDeviceTokens(): Promise { console.log("Mengambil token perangkat dari sumber data..."); // CONTOH: Ini adalah data dummy. Seharusnya Anda mengambilnya dari database. // Misalnya: const users = await prisma.user.findMany({ where: { fcmToken: { not: null } } }); // return users.map(user => user.fcmToken); const exampleTokens: string[] = [ "c89yuexsS_uc1tOErVPu5a:APA91bEb6tEKXAfReZjFVJ2mMyOzoW_RXryLSnSJTpbIVV3G0L_DCNkLuRvJ02Ip-Erz88QCQBAt-C2SN8eCRxu3-v1sBzXzKPtDv-huXpkjXsyrkifqvUo", // Valid "cRz96GHKTRaQaRJ35e8Hxa:APA91bEUSxE0VPbqKSzseQ_zGhbYsDofMexKykRw7o_3z2aPM9YFmZbeA2enrmb3qjdZ2g4-QQtiNHAyaZqAT1ITOrwo9jVJlShTeABmEFYP5GLEUZ3dlLc", // Valid "token_tidak_valid_ini_pasti_gagal_12345", // Contoh token tidak valid ]; console.log(`Berhasil mendapatkan ${exampleTokens.length} token.`); return exampleTokens; } /** * Membuat array pesan yang akan dikirim ke setiap token. * @param {string[]} tokens - Daftar token FCM. * @returns {Message[]} Array objek pesan untuk `sendEach`. */ function constructMessages(tokens: string[]): Message[] { return tokens.map((token) => ({ token, notification: { title: CONFIG.notificationPayload.title, body: CONFIG.notificationPayload.body, }, data: { // Anda bisa menambahkan data tambahan di sini // Contoh: click_action: "FLUTTER_NOTIFICATION_CLICK" timestamp: String(Date.now()), }, android: { priority: "high", notification: { sound: "default", channelId: "default_channel", // Pastikan channel ini ada di aplikasi Android Anda }, }, apns: { payload: { aps: { sound: "default", }, }, }, })); } /** * Menangani respons dari `sendEach` untuk mencatat keberhasilan dan kegagalan. * @param response - Objek respons dari `sendEach`. * @param originalTokens - Daftar token asli yang dikirimi pesan. */ function handleSendResponse(response: any, originalTokens: string[]) { console.log("Total notifikasi berhasil dikirim:", response.successCount); console.log("Total notifikasi gagal:", response.failureCount); const tokensToRemove: string[] = []; response.responses.forEach((resp: any, index: number) => { const token = originalTokens[index]; if (resp.success) { // console.log(`Pesan ke token ...${token.slice(-6)} berhasil:`, resp.messageId); } else { console.error(`Pesan ke token ...${token.slice(-6)} GAGAL:`, resp.error.code); // Jika token tidak lagi terdaftar, tandai untuk dihapus if ( resp.error.code === "messaging/registration-token-not-registered" || resp.error.code === "messaging/invalid-registration-token" ) { tokensToRemove.push(token); } } }); if (tokensToRemove.length > 0) { console.warn("Token berikut tidak valid dan harus dihapus dari database:"); tokensToRemove.forEach(token => console.log(`- ${token}`)); // TODO: Implementasikan logika untuk menghapus token-token di atas dari database Anda. // Misalnya: await prisma.user.updateMany({ where: { fcmToken: { in: tokensToRemove } }, data: { fcmToken: null } }); } } /** * Fungsi utama untuk mengirim notifikasi ke banyak perangkat dengan mekanisme retry. * @param {Message[]} messages - Array pesan yang akan dikirim. * @param {string[]} tokens - Daftar token asli untuk logging. */ async function sendNotifications(messages: Message[], tokens: string[]) { let lastError: any; for (let attempt = 1; attempt <= CONFIG.retry.maxRetries; attempt++) { try { const response = await getMessaging().sendEach(messages); handleSendResponse(response, tokens); return; // Berhasil, keluar dari fungsi } catch (error: any) { lastError = error; console.error(`Percobaan pengiriman ke-${attempt} gagal:`, error.message); // Hanya coba lagi jika error berhubungan dengan jaringan const isNetworkError = error.code === "app/network-error" || error.code?.includes("network"); if (isNetworkError && attempt < CONFIG.retry.maxRetries) { console.log(`Menunggu ${CONFIG.retry.delay}ms sebelum mencoba lagi...`); await new Promise((resolve) => setTimeout(resolve, CONFIG.retry.delay)); } else { // Jika bukan error jaringan atau sudah mencapai batas retry, lempar error throw new Error(`Gagal mengirim notifikasi setelah ${attempt} percobaan: ${error.message}`); } } } throw lastError; } /** * Fungsi orchestrator untuk menjalankan seluruh proses. */ export async function sendMultiple() { try { initializeFirebase(); const tokens = await getDeviceTokens(); if (tokens.length === 0) { console.log("Tidak ada token perangkat yang ditemukan. Proses dihentikan."); return; } const messages = constructMessages(tokens); console.log(` Mempersiapkan pengiriman ${messages.length} notifikasi...`); await sendNotifications(messages, tokens); console.log("Proses pengiriman notifikasi selesai."); } catch (error: any) { console.error("Terjadi error fatal dalam proses pengiriman:", error.message); process.exit(1); } } sendMultiple();