Initial commit with Flutter and Node.js code
This commit is contained in:
208
whatsapp_app/lib/services/whatsapp_service.dart
Normal file
208
whatsapp_app/lib/services/whatsapp_service.dart
Normal file
@@ -0,0 +1,208 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:web_socket_channel/web_socket_channel.dart';
|
||||
import '../config/app_config.dart';
|
||||
|
||||
enum WsStatus { disconnected, connecting, connected, waReady }
|
||||
|
||||
class WhatsAppService extends GetxService {
|
||||
// ── State ────────────────────────────────────────────────────────────────
|
||||
final status = WsStatus.disconnected.obs;
|
||||
final qrData = Rx<String?>(null);
|
||||
final isWaReady = false.obs;
|
||||
|
||||
// ── Internal ─────────────────────────────────────────────────────────────
|
||||
WebSocketChannel? _channel;
|
||||
StreamSubscription? _sub;
|
||||
int _reconnectCount = 0;
|
||||
Timer? _reconnectTimer;
|
||||
int _requestCounter = 0;
|
||||
|
||||
// Pending requests: requestId → Completer
|
||||
final Map<String, Completer<Map<String, dynamic>>> _pending = {};
|
||||
|
||||
// Event streams for push events (new messages, ack updates)
|
||||
final _eventController = StreamController<Map<String, dynamic>>.broadcast();
|
||||
Stream<Map<String, dynamic>> get events => _eventController.stream;
|
||||
|
||||
// ── Lifecycle ────────────────────────────────────────────────────────────
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
connect();
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
_sub?.cancel();
|
||||
_reconnectTimer?.cancel();
|
||||
_channel?.sink.close();
|
||||
_eventController.close();
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
// ── Connection ───────────────────────────────────────────────────────────
|
||||
void connect() {
|
||||
if (status.value == WsStatus.connecting ||
|
||||
status.value == WsStatus.connected ||
|
||||
status.value == WsStatus.waReady) return;
|
||||
|
||||
status.value = WsStatus.connecting;
|
||||
_reconnectTimer?.cancel();
|
||||
|
||||
try {
|
||||
_channel = WebSocketChannel.connect(Uri.parse(AppConfig.wsUrl));
|
||||
_sub?.cancel();
|
||||
_sub = _channel!.stream.listen(
|
||||
_onData,
|
||||
onError: _onError,
|
||||
onDone: _onDone,
|
||||
);
|
||||
status.value = WsStatus.connected;
|
||||
_reconnectCount = 0;
|
||||
|
||||
// Request initial status check
|
||||
ping();
|
||||
} catch (e) {
|
||||
_scheduleReconnect();
|
||||
}
|
||||
}
|
||||
|
||||
void _onData(dynamic raw) {
|
||||
Map<String, dynamic> data;
|
||||
try {
|
||||
data = jsonDecode(raw as String);
|
||||
} catch (_) {
|
||||
return;
|
||||
}
|
||||
|
||||
final type = data['type'] as String?;
|
||||
final requestId = data['requestId'] as String?;
|
||||
|
||||
// Resolve pending request
|
||||
if (requestId != null && _pending.containsKey(requestId)) {
|
||||
_pending.remove(requestId)!.complete(data);
|
||||
return;
|
||||
}
|
||||
|
||||
// Push events
|
||||
switch (type) {
|
||||
case 'qr':
|
||||
qrData.value = data['qr'];
|
||||
isWaReady.value = false;
|
||||
if (status.value == WsStatus.waReady) {
|
||||
status.value = WsStatus.connected;
|
||||
}
|
||||
break;
|
||||
case 'authenticated':
|
||||
qrData.value = null;
|
||||
break;
|
||||
case 'ready':
|
||||
isWaReady.value = true;
|
||||
status.value = WsStatus.waReady;
|
||||
qrData.value = null;
|
||||
break;
|
||||
case 'disconnected':
|
||||
isWaReady.value = false;
|
||||
status.value = WsStatus.connected;
|
||||
break;
|
||||
case 'status':
|
||||
if (data['ready'] == true) {
|
||||
isWaReady.value = true;
|
||||
status.value = WsStatus.waReady;
|
||||
qrData.value = null;
|
||||
} else {
|
||||
isWaReady.value = false;
|
||||
if (status.value == WsStatus.waReady) {
|
||||
status.value = WsStatus.connected;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Broadcast all events to listeners
|
||||
_eventController.add(data);
|
||||
}
|
||||
|
||||
void _onError(Object err) {
|
||||
_handleDisconnect();
|
||||
}
|
||||
|
||||
void _onDone() {
|
||||
_handleDisconnect();
|
||||
}
|
||||
|
||||
void _handleDisconnect() {
|
||||
status.value = WsStatus.disconnected;
|
||||
isWaReady.value = false;
|
||||
// Reject all pending requests with error
|
||||
final pendingKeys = List.from(_pending.keys);
|
||||
for (final key in pendingKeys) {
|
||||
_pending.remove(key)?.completeError(Exception('Connection lost'));
|
||||
}
|
||||
_scheduleReconnect();
|
||||
}
|
||||
|
||||
void _scheduleReconnect() {
|
||||
_reconnectTimer?.cancel();
|
||||
if (_reconnectCount >= AppConfig.maxReconnectAttempts) {
|
||||
print('[WS] Max reconnect attempts reached');
|
||||
return;
|
||||
}
|
||||
_reconnectCount++;
|
||||
_reconnectTimer = Timer(AppConfig.reconnectDelay, connect);
|
||||
}
|
||||
|
||||
// ── Request/Response ─────────────────────────────────────────────────────
|
||||
Future<Map<String, dynamic>> _request(Map<String, dynamic> payload) {
|
||||
final id = (_requestCounter++).toString();
|
||||
payload['requestId'] = id;
|
||||
final completer = Completer<Map<String, dynamic>>();
|
||||
|
||||
if (status.value == WsStatus.disconnected) {
|
||||
completer.completeError(Exception('WebSocket is disconnected'));
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
_pending[id] = completer;
|
||||
|
||||
try {
|
||||
_channel?.sink.add(jsonEncode(payload));
|
||||
} catch (e) {
|
||||
_pending.remove(id);
|
||||
completer.completeError(e);
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
// Timeout after 15s
|
||||
return completer.future.timeout(
|
||||
const Duration(seconds: 15),
|
||||
onTimeout: () {
|
||||
_pending.remove(id);
|
||||
throw TimeoutException('Request timed out: ${payload['type']}');
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// ── Public API ───────────────────────────────────────────────────────────
|
||||
Future<Map<String, dynamic>> getConversations({
|
||||
int limit = 50,
|
||||
int offset = 0,
|
||||
}) => _request({ 'type': 'get_conversations', 'limit': limit, 'offset': offset });
|
||||
|
||||
Future<Map<String, dynamic>> getMessages(String chatId, {int limit = 50}) =>
|
||||
_request({ 'type': 'get_messages', 'chatId': chatId, 'limit': limit });
|
||||
|
||||
Future<Map<String, dynamic>> sendMessage(String chatId, String text) =>
|
||||
_request({ 'type': 'send_message', 'chatId': chatId, 'text': text });
|
||||
|
||||
Future<Map<String, dynamic>> markRead(String chatId) =>
|
||||
_request({ 'type': 'mark_read', 'chatId': chatId });
|
||||
|
||||
Future<Map<String, dynamic>> searchConversations(String query) =>
|
||||
_request({ 'type': 'search_conversations', 'query': query });
|
||||
|
||||
Future<Map<String, dynamic>> ping() =>
|
||||
_request({ 'type': 'ping' });
|
||||
}
|
||||
Reference in New Issue
Block a user