import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; import 'package:Capsule/models/messages.dart'; import 'package:Capsule/models/text_messages.dart'; import 'package:mime/mime.dart'; import 'package:pointycastle/export.dart'; import 'package:sqflite/sqflite.dart'; import 'package:uuid/uuid.dart'; import '../utils/storage/write_file.dart'; import '/models/conversation_users.dart'; import '/models/friends.dart'; import '/models/my_profile.dart'; import '/utils/encryption/aes_helper.dart'; import '/utils/encryption/crypto_utils.dart'; import '/utils/storage/database.dart'; import '/utils/strings.dart'; Future createConversation(String title, List friends, bool twoUser) async { final db = await getDatabaseConnection(); MyProfile profile = await MyProfile.getProfile(); var uuid = const Uuid(); final String conversationId = uuid.v4(); Uint8List symmetricKey = AesHelper.deriveKey(generateRandomString(32)); Conversation conversation = Conversation( id: conversationId, userId: profile.id, symmetricKey: base64.encode(symmetricKey), admin: true, name: title, twoUser: twoUser, status: ConversationStatus.pending, isRead: true, messageExpiryDefault: 'no_expiry' ); await db.insert( 'conversations', conversation.toMap(), conflictAlgorithm: ConflictAlgorithm.replace, ); await db.insert( 'conversation_users', ConversationUser( id: uuid.v4(), userId: profile.id, conversationId: conversationId, username: profile.username, associationKey: uuid.v4(), publicKey: profile.publicKey!, admin: true, ).toMap(), conflictAlgorithm: ConflictAlgorithm.fail, ); for (Friend friend in friends) { await db.insert( 'conversation_users', ConversationUser( id: uuid.v4(), userId: friend.friendId, conversationId: conversationId, username: friend.username, associationKey: uuid.v4(), publicKey: friend.publicKey, admin: twoUser ? true : false, ).toMap(), conflictAlgorithm: ConflictAlgorithm.replace, ); } if (twoUser) { List> maps = await db.query( 'conversation_users', where: 'conversation_id = ? AND user_id != ?', whereArgs: [ conversation.id, profile.id ], ); if (maps.length != 1) { throw ArgumentError('Invalid user id'); } conversation.name = maps[0]['username']; await db.insert( 'conversations', conversation.toMap(), conflictAlgorithm: ConflictAlgorithm.replace, ); } return conversation; } 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; } 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'); } Future getConversationById(String id) async { final db = await getDatabaseConnection(); final List> maps = await db.query( 'conversations', where: 'id = ?', whereArgs: [id], ); if (maps.length != 1) { throw ArgumentError('Invalid user id'); } File? file; if (maps[0]['file'] != null && maps[0]['file'] != '') { file = File(maps[0]['file']); } return Conversation( id: maps[0]['id'], userId: maps[0]['user_id'], symmetricKey: maps[0]['symmetric_key'], admin: maps[0]['admin'] == 1, name: maps[0]['name'], twoUser: maps[0]['two_user'] == 1, status: ConversationStatus.values[maps[0]['status']], isRead: maps[0]['is_read'] == 1, icon: file, messageExpiryDefault: maps[0]['message_expiry'], ); } // A method that retrieves all the dogs from the dogs table. Future> getConversations() async { final db = await getDatabaseConnection(); final List> maps = await db.query( 'conversations', orderBy: 'name', ); return List.generate(maps.length, (i) { File? file; if (maps[i]['file'] != null && maps[i]['file'] != '') { file = File(maps[i]['file']); } return Conversation( id: maps[i]['id'], userId: maps[i]['user_id'], symmetricKey: maps[i]['symmetric_key'], admin: maps[i]['admin'] == 1, name: maps[i]['name'], twoUser: maps[i]['two_user'] == 1, status: ConversationStatus.values[maps[i]['status']], isRead: maps[i]['is_read'] == 1, icon: file, messageExpiryDefault: maps[i]['message_expiry'] ?? 'no_expiry', ); }); } Future getTwoUserConversation(String userId) async { final db = await getDatabaseConnection(); MyProfile profile = await MyProfile.getProfile(); final List> maps = await db.rawQuery( ''' SELECT conversations.* FROM conversations LEFT JOIN conversation_users ON conversation_users.conversation_id = conversations.id WHERE conversation_users.user_id = ? AND conversation_users.user_id != ? AND conversations.two_user = 1 ''', [ userId, profile.id ], ); if (maps.length != 1) { return null; } return Conversation( id: maps[0]['id'], userId: maps[0]['user_id'], symmetricKey: maps[0]['symmetric_key'], admin: maps[0]['admin'] == 1, name: maps[0]['name'], twoUser: maps[0]['two_user'] == 1, status: ConversationStatus.values[maps[0]['status']], isRead: maps[0]['is_read'] == 1, messageExpiryDefault: maps[0]['message_expiry'], ); } class Conversation { String id; String userId; String symmetricKey; bool admin; String name; bool twoUser; ConversationStatus status; bool isRead; String messageExpiryDefault = 'no_expiry'; File? icon; Conversation({ required this.id, required this.userId, required this.symmetricKey, required this.admin, required this.name, required this.twoUser, required this.status, required this.isRead, required this.messageExpiryDefault, this.icon, }); factory Conversation.fromJson(Map json, RSAPrivateKey privKey) { var symmetricKeyDecrypted = CryptoUtils.rsaDecrypt( base64.decode(json['symmetric_key']), privKey, ); var id = AesHelper.aesDecrypt( symmetricKeyDecrypted, base64.decode(json['conversation_detail_id']), ); var admin = AesHelper.aesDecrypt( symmetricKeyDecrypted, base64.decode(json['admin']), ); return Conversation( id: id, userId: json['user_id'], symmetricKey: base64.encode(symmetricKeyDecrypted), admin: admin == 'true', name: 'Unknown', twoUser: false, status: ConversationStatus.complete, isRead: true, messageExpiryDefault: 'no_expiry', ); } Future> payloadJson({ bool includeUsers = true }) async { MyProfile profile = await MyProfile.getProfile(); var symKey = base64.decode(symmetricKey); if (!includeUsers) { return { 'id': id, 'name': AesHelper.aesEncrypt(symKey, Uint8List.fromList(name.codeUnits)), 'users': await getEncryptedConversationUsers(this, symKey), }; } List users = await getConversationUsers(this); List userConversations = []; for (ConversationUser user in users) { RSAPublicKey pubKey = profile.publicKey!; String newId = id; if (profile.id != user.userId) { Friend friend = await getFriendByFriendId(user.userId); pubKey = friend.publicKey; newId = (const Uuid()).v4(); } userConversations.add({ 'id': newId, 'user_id': user.userId, 'conversation_detail_id': AesHelper.aesEncrypt(symKey, Uint8List.fromList(id.codeUnits)), 'admin': AesHelper.aesEncrypt(symKey, Uint8List.fromList((user.admin ? 'true' : 'false').codeUnits)), 'symmetric_key': base64.encode(CryptoUtils.rsaEncrypt(symKey, pubKey)), }); } Map returnData = { 'id': id, 'name': AesHelper.aesEncrypt(symKey, Uint8List.fromList(name.codeUnits)), 'users': await getEncryptedConversationUsers(this, symKey), 'two_user': AesHelper.aesEncrypt(symKey, Uint8List.fromList((twoUser ? 'true' : 'false').codeUnits)), 'message_expiry': messageExpiryDefault, 'user_conversations': userConversations, }; if (icon != null) { returnData['attachment'] = { 'data': AesHelper.aesEncrypt(symmetricKey, Uint8List.fromList(icon!.readAsBytesSync())), 'mimetype': lookupMimeType(icon!.path), 'extension': getExtension(icon!.path), }; } return returnData; } Map payloadImageJson() { if (icon == null) { return {}; } return { 'data': AesHelper.aesEncrypt(symmetricKey, Uint8List.fromList(icon!.readAsBytesSync())), 'mimetype': lookupMimeType(icon!.path), 'extension': getExtension(icon!.path), }; } Map toMap() { return { 'id': id, 'user_id': userId, 'symmetric_key': symmetricKey, 'admin': admin ? 1 : 0, 'name': name, 'two_user': twoUser ? 1 : 0, 'status': status.index, 'is_read': isRead ? 1 : 0, 'file': icon != null ? icon!.path : null, 'message_expiry': messageExpiryDefault, }; } @override String toString() { return ''' id: $id userId: $userId name: $name admin: $admin'''; } Future getRecentMessage() async { final db = await getDatabaseConnection(); final List> maps = await db.rawQuery( ''' SELECT * FROM messages WHERE association_key IN ( SELECT association_key FROM conversation_users WHERE conversation_id = ? ) ORDER BY created_at DESC LIMIT 1; ''', [ id ], ); if (maps.isEmpty) { return null; } return TextMessage( id: maps[0]['id'], symmetricKey: maps[0]['symmetric_key'], userSymmetricKey: maps[0]['user_symmetric_key'], text: maps[0]['data'] ?? 'Image', senderId: maps[0]['sender_id'], senderUsername: maps[0]['sender_username'], associationKey: maps[0]['association_key'], createdAt: maps[0]['created_at'], failedToSend: maps[0]['failed_to_send'] == 1, ); } } enum ConversationStatus { complete, pending, error, }