/// Datasource distant pour la communication (API) library messaging_remote_datasource; import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:http/http.dart' as http; import 'package:injectable/injectable.dart'; import '../../../../core/config/environment.dart'; import '../../../../core/error/exceptions.dart'; import '../../../authentication/data/datasources/keycloak_auth_service.dart'; import '../models/message_model.dart'; import '../models/conversation_model.dart'; import '../../domain/entities/message.dart'; @lazySingleton class MessagingRemoteDatasource { final http.Client client; final KeycloakAuthService authService; MessagingRemoteDatasource({ required this.client, required this.authService, }); /// Headers HTTP avec authentification — rafraîchit le token si expiré (fix IC-03) Future> _getHeaders() async { final token = await authService.getValidAccessToken(); return { 'Content-Type': 'application/json', 'Accept': 'application/json', if (token != null) 'Authorization': 'Bearer $token', }; } // === CONVERSATIONS === Future> getConversations({ String? organizationId, bool includeArchived = false, }) async { final uri = Uri.parse('${AppConfig.apiBaseUrl}/api/conversations') .replace(queryParameters: { if (organizationId != null) 'organisationId': organizationId, 'includeArchived': includeArchived.toString(), }); final response = await client.get(uri, headers: await _getHeaders()); if (response.statusCode == 200) { final List jsonList = json.decode(response.body); return jsonList .map((json) => ConversationModel.fromJson(json)) .toList(); } else if (response.statusCode == 401) { throw UnauthorizedException(); } else { throw ServerException('Erreur lors de la récupération des conversations'); } } Future getConversationById(String conversationId) async { final uri = Uri.parse( '${AppConfig.apiBaseUrl}/api/conversations/$conversationId'); final response = await client.get(uri, headers: await _getHeaders()); if (response.statusCode == 200) { return ConversationModel.fromJson(json.decode(response.body)); } else if (response.statusCode == 404) { throw NotFoundException('Conversation non trouvée'); } else if (response.statusCode == 401) { throw UnauthorizedException(); } else { throw ServerException('Erreur lors de la récupération de la conversation'); } } Future createConversation({ required String name, required List participantIds, String? organizationId, String? description, }) async { final uri = Uri.parse('${AppConfig.apiBaseUrl}/api/conversations'); final body = json.encode({ 'name': name, 'participantIds': participantIds, 'type': 'GROUP', // Default to GROUP for multi-participant conversations if (organizationId != null) 'organisationId': organizationId, if (description != null) 'description': description, }); final response = await client.post( uri, headers: await _getHeaders(), body: body, ); if (response.statusCode == 201 || response.statusCode == 200) { return ConversationModel.fromJson(json.decode(response.body)); } else if (response.statusCode == 401) { throw UnauthorizedException(); } else { throw ServerException('Erreur lors de la création de la conversation'); } } // === MESSAGES === Future> getMessages({ required String conversationId, int? limit, String? beforeMessageId, }) async { final uri = Uri.parse('${AppConfig.apiBaseUrl}/api/messages') .replace(queryParameters: { 'conversationId': conversationId, if (limit != null) 'limit': limit.toString(), // beforeMessageId not supported by backend yet, omit }); final response = await client.get(uri, headers: await _getHeaders()); if (response.statusCode == 200) { final List jsonList = json.decode(response.body); return jsonList.map((json) => MessageModel.fromJson(json)).toList(); } else if (response.statusCode == 401) { throw UnauthorizedException(); } else { throw ServerException('Erreur lors de la récupération des messages'); } } Future sendMessage({ required String conversationId, required String content, List? attachments, MessagePriority priority = MessagePriority.normal, }) async { final uri = Uri.parse('${AppConfig.apiBaseUrl}/api/messages'); final body = json.encode({ 'conversationId': conversationId, 'content': content, if (attachments != null) 'attachments': attachments, 'priority': priority.name.toUpperCase(), }); final response = await client.post( uri, headers: await _getHeaders(), body: body, ); if (response.statusCode == 201 || response.statusCode == 200) { return MessageModel.fromJson(json.decode(response.body)); } else if (response.statusCode == 401) { throw UnauthorizedException(); } else { throw ServerException('Erreur lors de l\'envoi du message'); } } Future sendBroadcast({ required String organizationId, required String subject, required String content, MessagePriority priority = MessagePriority.normal, List? attachments, }) async { final uri = Uri.parse('${AppConfig.apiBaseUrl}/api/messaging/broadcast'); final body = json.encode({ 'organizationId': organizationId, 'subject': subject, 'content': content, 'priority': priority.name, if (attachments != null) 'attachments': attachments, }); final response = await client.post( uri, headers: await _getHeaders(), body: body, ); if (response.statusCode == 201 || response.statusCode == 200) { return MessageModel.fromJson(json.decode(response.body)); } else if (response.statusCode == 401) { throw UnauthorizedException(); } else if (response.statusCode == 403) { throw ForbiddenException('Permission insuffisante pour envoyer un broadcast'); } else { throw ServerException('Erreur lors de l\'envoi du broadcast'); } } // === CONVERSATION ACTIONS === Future archiveConversation(String conversationId, {bool archive = true}) async { final uri = Uri.parse( '${AppConfig.apiBaseUrl}/api/conversations/$conversationId/archive') .replace(queryParameters: {'archive': archive.toString()}); final response = await client.put(uri, headers: await _getHeaders()); if (response.statusCode != 200 && response.statusCode != 204) { if (response.statusCode == 401) { throw UnauthorizedException(); } else { throw ServerException('Erreur lors de l\'archivage de la conversation'); } } } Future markConversationAsRead(String conversationId) async { final uri = Uri.parse( '${AppConfig.apiBaseUrl}/api/conversations/$conversationId/mark-read'); final response = await client.put(uri, headers: await _getHeaders()); if (response.statusCode != 200 && response.statusCode != 204) { if (response.statusCode == 401) { throw UnauthorizedException(); } else { throw ServerException('Erreur lors du marquage de la conversation comme lue'); } } } Future toggleMuteConversation(String conversationId) async { final uri = Uri.parse( '${AppConfig.apiBaseUrl}/api/conversations/$conversationId/toggle-mute'); final response = await client.put(uri, headers: await _getHeaders()); if (response.statusCode != 200 && response.statusCode != 204) { if (response.statusCode == 401) { throw UnauthorizedException(); } else { throw ServerException('Erreur lors du toggle mute de la conversation'); } } } Future togglePinConversation(String conversationId) async { final uri = Uri.parse( '${AppConfig.apiBaseUrl}/api/conversations/$conversationId/toggle-pin'); final response = await client.put(uri, headers: await _getHeaders()); if (response.statusCode != 200 && response.statusCode != 204) { if (response.statusCode == 401) { throw UnauthorizedException(); } else { throw ServerException('Erreur lors du toggle pin de la conversation'); } } } // === MESSAGE ACTIONS === Future editMessage({ required String messageId, required String newContent, }) async { final uri = Uri.parse('${AppConfig.apiBaseUrl}/api/messages/$messageId'); final body = json.encode({'content': newContent}); final response = await client.put( uri, headers: await _getHeaders(), body: body, ); if (response.statusCode == 200) { return MessageModel.fromJson(json.decode(response.body)); } else if (response.statusCode == 401) { throw UnauthorizedException(); } else if (response.statusCode == 404) { throw NotFoundException('Message non trouvé'); } else { throw ServerException('Erreur lors de l\'édition du message'); } } Future deleteMessage(String messageId) async { final uri = Uri.parse('${AppConfig.apiBaseUrl}/api/messages/$messageId'); final response = await client.delete(uri, headers: await _getHeaders()); if (response.statusCode != 200 && response.statusCode != 204) { if (response.statusCode == 401) { throw UnauthorizedException(); } else if (response.statusCode == 404) { throw NotFoundException('Message non trouvé'); } else { throw ServerException('Erreur lors de la suppression du message'); } } } Future markMessageAsRead(String messageId) async { // Backend has no per-message read endpoint — use markConversationAsRead if (AppConfig.enableLogging) { debugPrint('[Messaging] markMessageAsRead ignored (no per-message endpoint), messageId=$messageId'); } } Future getUnreadCount({String? organizationId}) async { try { final conversations = await getConversations(organizationId: organizationId); return conversations.fold(0, (sum, c) => sum + c.unreadCount); } catch (_) { return 0; } } }