1import 'package:dio/dio.dart'; 2import 'package:flutter/foundation.dart'; 3 4import '../config/environment_config.dart'; 5import '../models/coves_session.dart'; 6import 'api_exceptions.dart'; 7import 'auth_interceptor.dart'; 8 9/// Comment Service 10/// 11/// Handles comment creation through the Coves backend. 12/// 13/// **Architecture with Backend OAuth**: 14/// With sealed tokens, the client cannot write directly to the user's PDS 15/// (no DPoP keys available). Instead, comments go through the Coves backend: 16/// 17/// Mobile Client → Coves Backend (sealed token) → User's PDS (DPoP) 18/// 19/// The backend: 20/// 1. Unseals the token to get the actual access/refresh tokens 21/// 2. Uses stored DPoP keys to sign requests 22/// 3. Writes to the user's PDS on their behalf 23/// 24/// **Backend Endpoint**: 25/// - POST /xrpc/social.coves.community.comment.create 26class CommentService { 27 CommentService({ 28 Future<CovesSession?> Function()? sessionGetter, 29 Future<bool> Function()? tokenRefresher, 30 Future<void> Function()? signOutHandler, 31 Dio? dio, 32 }) : _sessionGetter = sessionGetter { 33 _dio = 34 dio ?? 35 Dio( 36 BaseOptions( 37 baseUrl: EnvironmentConfig.current.apiUrl, 38 connectTimeout: const Duration(seconds: 30), 39 receiveTimeout: const Duration(seconds: 30), 40 headers: {'Content-Type': 'application/json'}, 41 ), 42 ); 43 44 // Add shared 401 retry interceptor 45 _dio.interceptors.add( 46 createAuthInterceptor( 47 sessionGetter: sessionGetter, 48 tokenRefresher: tokenRefresher, 49 signOutHandler: signOutHandler, 50 serviceName: 'CommentService', 51 dio: _dio, 52 ), 53 ); 54 } 55 56 final Future<CovesSession?> Function()? _sessionGetter; 57 late final Dio _dio; 58 59 /// Create a comment 60 /// 61 /// Sends comment request to the Coves backend, which writes to the 62 /// user's PDS. 63 /// 64 /// Parameters: 65 /// - [rootUri]: AT-URI of the root post (always the original post) 66 /// - [rootCid]: CID of the root post 67 /// - [parentUri]: AT-URI of the parent (post or comment) 68 /// - [parentCid]: CID of the parent 69 /// - [content]: Comment text content 70 /// 71 /// Returns: 72 /// - CreateCommentResponse with uri and cid of the created comment 73 /// 74 /// Throws: 75 /// - ApiException for API errors 76 /// - AuthenticationException for auth failures 77 Future<CreateCommentResponse> createComment({ 78 required String rootUri, 79 required String rootCid, 80 required String parentUri, 81 required String parentCid, 82 required String content, 83 }) async { 84 try { 85 final session = await _sessionGetter?.call(); 86 87 if (session == null) { 88 throw AuthenticationException( 89 'User not authenticated - no session available', 90 ); 91 } 92 93 if (kDebugMode) { 94 debugPrint('💬 Creating comment via backend'); 95 debugPrint(' Root: $rootUri'); 96 debugPrint(' Parent: $parentUri'); 97 debugPrint(' Content length: ${content.length}'); 98 } 99 100 // Send comment request to backend 101 // Note: Authorization header is added by the interceptor 102 final response = await _dio.post<Map<String, dynamic>>( 103 '/xrpc/social.coves.community.comment.create', 104 data: { 105 'reply': { 106 'root': {'uri': rootUri, 'cid': rootCid}, 107 'parent': {'uri': parentUri, 'cid': parentCid}, 108 }, 109 'content': content, 110 }, 111 ); 112 113 final data = response.data; 114 if (data == null) { 115 throw ApiException('Invalid response from server - no data'); 116 } 117 118 final uri = data['uri'] as String?; 119 final cid = data['cid'] as String?; 120 121 if (uri == null || uri.isEmpty || cid == null || cid.isEmpty) { 122 throw ApiException( 123 'Invalid response from server - missing uri or cid', 124 ); 125 } 126 127 if (kDebugMode) { 128 debugPrint('✅ Comment created: $uri'); 129 } 130 131 return CreateCommentResponse(uri: uri, cid: cid); 132 } on DioException catch (e) { 133 if (kDebugMode) { 134 debugPrint('❌ Comment creation failed: ${e.message}'); 135 debugPrint(' Status: ${e.response?.statusCode}'); 136 debugPrint(' Data: ${e.response?.data}'); 137 } 138 139 if (e.response?.statusCode == 401) { 140 throw AuthenticationException( 141 'Authentication failed. Please sign in again.', 142 originalError: e, 143 ); 144 } 145 146 throw ApiException( 147 'Failed to create comment: ${e.message}', 148 statusCode: e.response?.statusCode, 149 originalError: e, 150 ); 151 } on AuthenticationException { 152 rethrow; 153 } on ApiException { 154 rethrow; 155 } on Exception catch (e) { 156 throw ApiException('Failed to create comment: $e'); 157 } 158 } 159} 160 161/// Response from comment creation 162class CreateCommentResponse { 163 const CreateCommentResponse({required this.uri, required this.cid}); 164 165 /// AT-URI of the created comment record 166 final String uri; 167 168 /// CID of the created comment record 169 final String cid; 170}