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/
|
.idea/
|
||||||
.vscode/
|
.vscode/
|
||||||
*.swp
|
*.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) {
|
void _onNotificationTap(NotificationResponse response) {
|
||||||
if (response.payload != null) {
|
if (response.payload != null) {
|
||||||
final data = jsonDecode(response.payload!);
|
final data = jsonDecode(response.payload!);
|
||||||
@@ -165,7 +195,6 @@ class FirebaseService extends GetxService {
|
|||||||
final name = data['name'] ?? 'Chat';
|
final name = data['name'] ?? 'Chat';
|
||||||
|
|
||||||
if (chatId != null) {
|
if (chatId != null) {
|
||||||
// Mock a conversation model to navigate to ChatScreen
|
|
||||||
final dummyChat = ConversationModel(
|
final dummyChat = ConversationModel(
|
||||||
id: chatId,
|
id: chatId,
|
||||||
name: name,
|
name: name,
|
||||||
@@ -176,7 +205,7 @@ class FirebaseService extends GetxService {
|
|||||||
isMuted: false,
|
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:get/get.dart';
|
||||||
import 'package:web_socket_channel/web_socket_channel.dart';
|
import 'package:web_socket_channel/web_socket_channel.dart';
|
||||||
import '../config/app_config.dart';
|
import '../config/app_config.dart';
|
||||||
|
import 'firebase_service.dart';
|
||||||
|
|
||||||
enum WsStatus { disconnected, connecting, connected, waReady }
|
enum WsStatus { disconnected, connecting, connected, waReady }
|
||||||
|
|
||||||
@@ -93,7 +94,28 @@ class WhatsAppService extends GetxService {
|
|||||||
|
|
||||||
// Push events
|
// Push events
|
||||||
switch (type) {
|
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':
|
case 'qr':
|
||||||
|
|
||||||
qrData.value = data['qr'];
|
qrData.value = data['qr'];
|
||||||
isWaReady.value = false;
|
isWaReady.value = false;
|
||||||
if (status.value == WsStatus.waReady) {
|
if (status.value == WsStatus.waReady) {
|
||||||
|
|||||||
@@ -14,7 +14,9 @@
|
|||||||
"author": "Antigravity Dev Team",
|
"author": "Antigravity Dev Team",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"dotenv": "^16.4.5",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
|
"firebase-admin": "^11.11.1",
|
||||||
"puppeteer": "^21.0.0",
|
"puppeteer": "^21.0.0",
|
||||||
"qrcode": "^1.5.3",
|
"qrcode": "^1.5.3",
|
||||||
"whatsapp-web.js": "^1.26.0",
|
"whatsapp-web.js": "^1.26.0",
|
||||||
|
|||||||
@@ -16,6 +16,120 @@ const app = express();
|
|||||||
const server = http.createServer(app);
|
const server = http.createServer(app);
|
||||||
const wss = new WebSocketServer({ server });
|
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 ─────────────────────────────────────────────────────────────────
|
// ─── State ─────────────────────────────────────────────────────────────────
|
||||||
let waClient = null;
|
let waClient = null;
|
||||||
let clientReady = false;
|
let clientReady = false;
|
||||||
@@ -216,6 +330,21 @@ function initWhatsApp() {
|
|||||||
try {
|
try {
|
||||||
const formatted = formatMessage(msg);
|
const formatted = formatMessage(msg);
|
||||||
broadcast({ type: 'new_message', chatId: msg.from, data: formatted });
|
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) {
|
} catch (err) {
|
||||||
console.error('[WA] Error formatting new message event:', err.message);
|
console.error('[WA] Error formatting new message event:', err.message);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user