Compare commits

...

4 Commits

5 changed files with 190 additions and 2 deletions

6
.gitignore vendored
View File

@@ -24,3 +24,9 @@ whatsapp_app/android/local.properties
.idea/
.vscode/
*.swp
# Sensitive Configurations
whatsapp_bridge/serviceAccountKey.json
whatsapp_bridge/fcm_token.json
whatsapp_bridge/.env
whatsapp_bridge/.env.*

View File

@@ -153,6 +153,36 @@ class FirebaseService extends GetxService {
);
}
void showLocalNotificationFromData(Map<String, dynamic> data) {
final chatId = data['chatId'];
final name = data['name'] ?? 'WhatsApp';
final body = data['body'] ?? 'New Message';
// Smart Notification: Only show if we are NOT currently in this chat
final activeChatId = Get.find<WhatsAppService>().activeChatId.value;
if (chatId != null && activeChatId == chatId) {
return; // Silent
}
const androidDetails = AndroidNotificationDetails(
'whatsapp_channel',
'WhatsApp Messages',
importance: Importance.max,
priority: Priority.high,
ticker: 'ticker',
);
const iosDetails = DarwinNotificationDetails();
const details = NotificationDetails(android: androidDetails, iOS: iosDetails);
_localNotifications.show(
DateTime.now().microsecond,
name,
body,
details,
payload: jsonEncode({'chatId': chatId, 'name': name}),
);
}
void _onNotificationTap(NotificationResponse response) {
if (response.payload != null) {
final data = jsonDecode(response.payload!);
@@ -165,7 +195,6 @@ class FirebaseService extends GetxService {
final name = data['name'] ?? 'Chat';
if (chatId != null) {
// Mock a conversation model to navigate to ChatScreen
final dummyChat = ConversationModel(
id: chatId,
name: name,
@@ -176,7 +205,7 @@ class FirebaseService extends GetxService {
isMuted: false,
);
Get.to(() => ChatScreen(conversation: dummyChat));
Get.to(() => ChatScreen(conversation: dummyChat), preventDuplicates: false);
}
}
}

View File

@@ -3,6 +3,7 @@ import 'dart:convert';
import 'package:get/get.dart';
import 'package:web_socket_channel/web_socket_channel.dart';
import '../config/app_config.dart';
import 'firebase_service.dart';
enum WsStatus { disconnected, connecting, connected, waReady }
@@ -93,7 +94,28 @@ class WhatsAppService extends GetxService {
// Push events
switch (type) {
case 'new_message':
// Trigger a local notification if the app is open (WebSocket connected)
final chatId = data['chatId'];
final msgData = data['data'];
if (msgData != null && msgData['fromMe'] != true) {
String body = msgData['body'] ?? '';
if (body.isEmpty && msgData['hasMedia'] == true) {
body = '📷 Media/Audio message';
}
try {
Get.find<FirebaseService>().showLocalNotificationFromData({
'chatId': chatId,
'name': chatId?.split('@')[0] ?? 'WhatsApp',
'body': body,
});
} catch (e) {
print('[LOCAL NOTIF ERROR] $e');
}
}
break;
case 'qr':
qrData.value = data['qr'];
isWaReady.value = false;
if (status.value == WsStatus.waReady) {

View File

@@ -14,7 +14,9 @@
"author": "Antigravity Dev Team",
"license": "ISC",
"dependencies": {
"dotenv": "^16.4.5",
"express": "^4.18.2",
"firebase-admin": "^11.11.1",
"puppeteer": "^21.0.0",
"qrcode": "^1.5.3",
"whatsapp-web.js": "^1.26.0",

View File

@@ -16,6 +16,120 @@ const app = express();
const server = http.createServer(app);
const wss = new WebSocketServer({ server });
// Load environment variables from .env file (Smart Multi-Path Lookup)
const path = require('path');
const fs = require('fs');
const dotenv = require('dotenv');
const envPaths = [
path.join(__dirname, '.env'), // whatsapp_bridge/.env
path.join(__dirname, '..', '.env'), // mywhatsapp.intaleqapp.com/.env
'/home/intaleqapp-mywhatsapp/.env' // Server user root-level .env (as in user screenshot)
];
let envLoaded = false;
for (const envPath of envPaths) {
if (fs.existsSync(envPath)) {
dotenv.config({ path: envPath });
console.log(`[ENV] Successfully loaded environment variables from: ${envPath}`);
envLoaded = true;
break;
}
}
if (!envLoaded) {
dotenv.config(); // Fallback to default CWD
console.log('[ENV] No specific .env found in known paths, loaded default configuration');
}
// ─── Firebase Admin SDK Configuration (Highly Secure Background Pushes) ─────
const admin = require('firebase-admin');
let firebaseApp = null;
// Support three secure options:
// 1. Raw JSON string in environment variable (FIREBASE_SERVICE_ACCOUNT)
// 2. Custom secure file path in environment variable (FIREBASE_SERVICE_ACCOUNT_PATH)
// 3. Fallback local file ignored by Git (serviceAccountKey.json)
const envServiceAccount = process.env.FIREBASE_SERVICE_ACCOUNT;
const envServiceAccountPath = process.env.FIREBASE_SERVICE_ACCOUNT_PATH;
const localServiceAccountPath = path.join(__dirname, 'serviceAccountKey.json');
try {
if (envServiceAccount) {
let serviceAccount;
if (envServiceAccount.trim().startsWith('{')) {
serviceAccount = JSON.parse(envServiceAccount);
console.log('[FCM] Initializing Firebase Admin SDK via direct env JSON string...');
} else {
serviceAccount = require(envServiceAccount);
console.log(`[FCM] Initializing Firebase Admin SDK via custom path from env: ${envServiceAccount}`);
}
firebaseApp = admin.initializeApp({
credential: admin.credential.cert(serviceAccount)
});
} else if (envServiceAccountPath && fs.existsSync(envServiceAccountPath)) {
console.log(`[FCM] Initializing Firebase Admin SDK via secure custom path: ${envServiceAccountPath}`);
const serviceAccount = require(envServiceAccountPath);
firebaseApp = admin.initializeApp({
credential: admin.credential.cert(serviceAccount)
});
} else if (fs.existsSync(localServiceAccountPath)) {
console.log('[FCM] Initializing Firebase Admin SDK via fallback local serviceAccountKey.json...');
const serviceAccount = require(localServiceAccountPath);
firebaseApp = admin.initializeApp({
credential: admin.credential.cert(serviceAccount)
});
} else {
console.warn('[FCM WARNING] No Firebase Service Account found in environment or local files. Background push notifications will be disabled.');
}
} catch (err) {
console.error('[FCM ERROR] Failed to initialize Firebase Admin SDK:', err.message);
}
async function sendPushNotification(chatId, senderName, body) {
if (!firebaseApp) {
console.log('[FCM] Push skipped: Firebase Admin SDK not initialized.');
return;
}
const tokenPath = path.join(__dirname, 'fcm_token.json');
if (!fs.existsSync(tokenPath)) {
console.log('[FCM] Push skipped: No registered FCM device token found.');
return;
}
try {
const tokenData = JSON.parse(fs.readFileSync(tokenPath));
const token = tokenData.token;
if (!token) return;
const message = {
token: token,
notification: {
title: senderName || 'WhatsApp Message',
body: body || 'New Message'
},
data: {
chatId: chatId,
name: senderName || 'WhatsApp'
},
apns: {
payload: {
aps: {
sound: 'default',
badge: 1
}
}
}
};
const response = await admin.messaging().send(message);
console.log('[FCM] Push notification sent successfully, messageId:', response);
} catch (err) {
console.error('[FCM SEND ERROR] Failed to send push notification:', err.message);
}
}
// ─── State ─────────────────────────────────────────────────────────────────
let waClient = null;
let clientReady = false;
@@ -216,6 +330,21 @@ function initWhatsApp() {
try {
const formatted = formatMessage(msg);
broadcast({ type: 'new_message', chatId: msg.from, data: formatted });
// Trigger background push notification if not sent by me
if (!msg.fromMe) {
try {
const contact = await msg.getContact();
const senderName = contact.name || contact.pushname || msg.from.split('@')[0];
let body = msg.body || '';
if (!body && msg.hasMedia) {
body = '📷 Media/Attachment';
}
await sendPushNotification(msg.from, senderName, body);
} catch (fcmErr) {
console.error('[FCM PUSH ERROR] Failed to send background push:', fcmErr.message);
}
}
} catch (err) {
console.error('[WA] Error formatting new message event:', err.message);
}