import 'dart:convert'; import 'package:Envelope/utils/storage/get_file.dart'; import 'package:http/http.dart' as http; import 'package:pointycastle/export.dart'; import 'package:sqflite/sqflite.dart'; import 'package:uuid/uuid.dart'; import '/database/models/friends.dart'; import '/database/models/conversation_users.dart'; import '/database/models/conversations.dart'; import '/database/models/my_profile.dart'; import '/exceptions/update_data_exception.dart'; import '/utils/encryption/aes_helper.dart'; import '/utils/storage/database.dart'; import '/utils/storage/session_cookie.dart'; class _BaseConversationsResult { List conversations; List detailIds; _BaseConversationsResult({ required this.conversations, required this.detailIds, }); } class ConversationsService { static Future saveConversation(Conversation conversation) async { final db = await getDatabaseConnection(); db.update( 'conversations', conversation.toMap(), where: 'id = ?', whereArgs: [conversation.id], ); } static Future addUsersToConversation(Conversation conversation, List friends) async { final db = await getDatabaseConnection(); var uuid = const Uuid(); for (Friend friend in friends) { await db.insert( 'conversation_users', ConversationUser( id: uuid.v4(), userId: friend.friendId, conversationId: conversation.id, username: friend.username, associationKey: uuid.v4(), publicKey: friend.publicKey, admin: false, ).toMap(), conflictAlgorithm: ConflictAlgorithm.replace, ); } return conversation; } static Future updateConversation( Conversation conversation, { includeUsers = false, updatedImage = false, saveConversation = true, } ) async { if (saveConversation) { await saveConversation(conversation); } String sessionCookie = await getSessionCookie(); Map conversationJson = await conversation.payloadJson(includeUsers: includeUsers); var resp = await http.put( await MyProfile.getServerUrl('api/v1/auth/conversations'), headers: { 'Content-Type': 'application/json; charset=UTF-8', 'cookie': sessionCookie, }, body: jsonEncode(conversationJson), ); if (resp.statusCode != 204) { throw UpdateDataException('Unable to update conversation, please try again later.'); } if (!updatedImage) { return; } Map attachmentJson = conversation.payloadImageJson(); resp = await http.post( await MyProfile.getServerUrl('api/v1/auth/conversations/${conversation.id}/image'), headers: { 'Content-Type': 'application/json; charset=UTF-8', 'cookie': sessionCookie, }, body: jsonEncode(attachmentJson), ); if (resp.statusCode != 204) { throw UpdateDataException('Unable to update conversation image, please try again later.'); } } static Future<_BaseConversationsResult> _getBaseConversations() async { RSAPrivateKey privKey = await MyProfile.getPrivateKey(); http.Response resp = await http.get( await MyProfile.getServerUrl('api/v1/auth/conversations'), headers: { 'cookie': await getSessionCookie(), } ); if (resp.statusCode != 200) { throw Exception(resp.body); } _BaseConversationsResult result = _BaseConversationsResult( conversations: [], detailIds: [] ); List conversationsJson = jsonDecode(resp.body); if (conversationsJson.isEmpty) { return result; } for (var i = 0; i < conversationsJson.length; i++) { Conversation conversation = Conversation.fromJson( conversationsJson[i] as Map, privKey, ); result.conversations.add(conversation); result.detailIds.add(conversation.id); } return result; } static Conversation _findConversationByDetailId(List conversations, String id) { for (var conversation in conversations) { if (conversation.id == id) { return conversation; } } // Or return `null`. throw ArgumentError.value(id, 'id', 'No element with that id'); } static Future _storeConversations(Database db, Conversation conversation, Map detailsJson) async { conversation.createdAt = DateTime.parse(detailsJson['created_at']); conversation.updatedAt = DateTime.parse(detailsJson['updated_at']); conversation.messageExpiryDefault = detailsJson['message_expiry']; conversation.twoUser = AesHelper.aesDecrypt( base64.decode(conversation.symmetricKey), base64.decode(detailsJson['two_user']), ) == 'true'; if (conversation.twoUser) { MyProfile profile = await MyProfile.getProfile(); final db = await getDatabaseConnection(); List> maps = await db.query( 'conversation_users', where: 'conversation_id = ? AND user_id != ?', whereArgs: [ conversation.id, profile.id ], ); if (maps.length != 1) { conversation.name = 'TODO: Fix this'; } else { conversation.name = maps[0]['username']; } } else { conversation.name = AesHelper.aesDecrypt( base64.decode(conversation.symmetricKey), base64.decode(detailsJson['name']), ); } if (detailsJson['attachment_id'] != null) { conversation.icon = await getFile( '$defaultServerUrl/files/${detailsJson['attachment']['image_link']}', conversation.id, conversation.symmetricKey, ).catchError((dynamic) async {}); } await db.insert( 'conversations', conversation.toMap(), conflictAlgorithm: ConflictAlgorithm.replace, ); List usersData = detailsJson['users']; for (var i = 0; i < usersData.length; i++) { ConversationUser conversationUser = ConversationUser.fromJson( usersData[i] as Map, base64.decode(conversation.symmetricKey), ); await db.insert( 'conversation_users', conversationUser.toMap(), conflictAlgorithm: ConflictAlgorithm.replace, ); } } static Future updateConversations({DateTime? updatedAt}) async { _BaseConversationsResult baseConvs = await _getBaseConversations(); if (baseConvs.detailIds.isEmpty) { return; } Map params = {}; if (updatedAt != null) { params['updated_at'] = updatedAt.toIso8601String(); } params['conversation_detail_ids'] = baseConvs.detailIds.join(','); var uri = await MyProfile.getServerUrl('api/v1/auth/conversation_details'); uri = uri.replace(queryParameters: params); http.Response resp = await http.get( uri, headers: { 'cookie': await getSessionCookie(), } ); if (resp.statusCode != 200) { throw Exception(resp.body); } final db = await getDatabaseConnection(); List conversationsDetailsJson = jsonDecode(resp.body); for (var i = 0; i < conversationsDetailsJson.length; i++) { Map detailsJson = conversationsDetailsJson[i] as Map; Conversation conversation = _findConversationByDetailId(baseConvs.conversations, detailsJson['id']); await _storeConversations(db, conversation, detailsJson); } } static Future uploadConversation(Conversation conversation) async { String sessionCookie = await getSessionCookie(); Map conversationJson = await conversation.payloadJson(); var resp = await http.post( await MyProfile.getServerUrl('api/v1/auth/conversations'), headers: { 'Content-Type': 'application/json; charset=UTF-8', 'cookie': sessionCookie, }, body: jsonEncode(conversationJson), ); if (resp.statusCode != 204) { throw Exception('Failed to create conversation'); } } static Future updateMessageExpiry(String id, String messageExpiry) async { http.Response resp = await http.post( await MyProfile.getServerUrl( 'api/v1/auth/conversations/$id/message_expiry' ), headers: { 'cookie': await getSessionCookie(), }, body: jsonEncode({ 'message_expiry': messageExpiry, }), ); if (resp.statusCode == 204) { return; } throw Exception('Cannot set message expiry for conversation'); } }