Initial commit with Flutter and Node.js code

This commit is contained in:
Hamza-Ayed
2026-05-18 14:04:39 +03:00
commit a60a173b51
21 changed files with 3107 additions and 0 deletions

View 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' });
}