Compare commits
4 Commits
cfc1fd0a8e
...
b3ef0b89f6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b3ef0b89f6 | ||
|
|
6882d6e952 | ||
|
|
79ba52cb7d | ||
|
|
92d59b0f30 |
6
.gitignore
vendored
6
.gitignore
vendored
@@ -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.*
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user