import 'dart:async'; import 'dart:convert'; import 'dart:typed_data'; import 'package:flutter/foundation.dart'; import 'package:web_socket_channel/web_socket_channel.dart'; import '../models/message.dart'; enum ConnectionState { disconnected, connecting, connected } class ConnectionService extends ChangeNotifier { WebSocketChannel? _channel; ConnectionState _state = ConnectionState.disconnected; String _serverUrl = 'ws://localhost:8443/ws'; String? _userId; String? _username; String? _token; Timer? _heartbeatTimer; int _seq = 0; final StreamController> _messageController = StreamController>.broadcast(); Stream> get messageStream => _messageController.stream; ConnectionState get state => _state; String? get userId => _userId; String? get username => _username; String? get token => _token; bool get isConnected => _state == ConnectionState.connected; String get serverUrl => _serverUrl; set serverUrl(String url) { _serverUrl = url; notifyListeners(); } @override void dispose() { disconnect(); _messageController.close(); super.dispose(); } void init() { notifyListeners(); } Future connect() async { _state = ConnectionState.connecting; notifyListeners(); try { _channel = WebSocketChannel.connect(Uri.parse(_serverUrl)); await _channel!.ready; _channel!.stream.listen( (data) { _handleMessage(data); }, onError: (error) { _state = ConnectionState.disconnected; notifyListeners(); }, onDone: () { _state = ConnectionState.disconnected; _heartbeatTimer?.cancel(); notifyListeners(); }, ); _state = ConnectionState.connected; _startHeartbeat(); notifyListeners(); } catch (e) { _state = ConnectionState.disconnected; notifyListeners(); rethrow; } } void disconnect() { _heartbeatTimer?.cancel(); _channel?.sink.close(); _channel = null; _state = ConnectionState.disconnected; notifyListeners(); } void _startHeartbeat() { _heartbeatTimer = Timer.periodic(const Duration(seconds: 30), (_) { send({'op': 0}); }); } void _handleMessage(dynamic data) { try { final Map packet = jsonDecode(data as String); final int op = packet['op'] as int; switch (op) { case 1: // Hello break; case 3: // Authenticated final d = packet['d'] as Map; _userId = d['user_id'] as String?; _username = d['username'] as String?; _token = d['token'] as String?; notifyListeners(); break; case 10: // MessageCreate _messageController.add(packet['d'] as Map); break; case 13: // Reaction _messageController.add(packet['d'] as Map); break; case 40: // VoiceState _messageController.add(packet['d'] as Map); break; case 60: // Typing _messageController.add(packet['d'] as Map); break; } } catch (e) { debugPrint('Failed to handle message: $e'); } } void send(Map packet) { if (_channel == null) return; packet['s'] = ++_seq; _channel!.sink.add(jsonEncode(packet)); } void authenticate({String? token, String? username}) { send({ 'op': 2, 'd': { if (token != null) 'token': token, if (username != null) 'username': username, }, }); } void sendMessage({ required String channelId, required Uint8List content, bool encrypted = false, Uint8List? nonce, String messageType = 'text', String? replyTo, }) { send({ 'op': 10, 'd': { 'channel_id': channelId, 'content': content.toList(), 'encrypted': encrypted, if (nonce != null) 'nonce': nonce.toList(), 'message_type': messageType, if (replyTo != null) 'reply_to': replyTo, }, }); } void sendReaction(String messageId, String emoji, {bool add = true}) { send({ 'op': 13, 'd': { 'message_id': messageId, 'emoji': emoji, 'add': add, }, }); } void updateVoiceState({ required String guildId, String? channelId, bool muted = false, bool deafened = false, }) { send({ 'op': 40, 'd': { 'guild_id': guildId, 'channel_id': channelId ?? '', 'muted': muted, 'deafened': deafened, }, }); } void sendTyping(String channelId) { send({ 'op': 60, 'd': {'channel_id': channelId}, }); } }