From 8b0ab86c83cf61261f7f2e9e6e583c5edf074f3e Mon Sep 17 00:00:00 2001 From: Tovi Jaeschke-Rogers Date: Fri, 28 Oct 2022 08:36:08 +1030 Subject: [PATCH] Add repository classes to interact with DB --- .../database/models/conversation_users.dart | 93 +------- mobile/lib/database/models/conversations.dart | 212 +----------------- mobile/lib/database/models/friends.dart | 56 +---- mobile/lib/database/models/messages.dart | 55 +---- .../conversation_users_repository.dart | 97 ++++++++ .../conversations_repository.dart | 206 +++++++++++++++++ .../repositories/friends_repository.dart | 62 +++++ .../repositories/messages_repository.dart | 54 +++++ mobile/lib/services/messages_service.dart | 10 +- .../lib/views/main/conversation/detail.dart | 5 +- mobile/lib/views/main/conversation/list.dart | 8 +- .../views/main/conversation/list_item.dart | 3 +- .../lib/views/main/conversation/settings.dart | 3 +- mobile/lib/views/main/friend/list.dart | 5 +- mobile/lib/views/main/friend/list_item.dart | 5 +- mobile/lib/views/main/home.dart | 14 +- 16 files changed, 466 insertions(+), 422 deletions(-) create mode 100644 mobile/lib/database/repositories/conversation_users_repository.dart create mode 100644 mobile/lib/database/repositories/conversations_repository.dart create mode 100644 mobile/lib/database/repositories/friends_repository.dart create mode 100644 mobile/lib/database/repositories/messages_repository.dart diff --git a/mobile/lib/database/models/conversation_users.dart b/mobile/lib/database/models/conversation_users.dart index f8d74d6..02e4baf 100644 --- a/mobile/lib/database/models/conversation_users.dart +++ b/mobile/lib/database/models/conversation_users.dart @@ -3,101 +3,10 @@ import 'dart:typed_data'; import 'package:pointycastle/impl.dart'; -import '/database/models/conversations.dart'; -import '/utils/storage/database.dart'; import '/utils/encryption/aes_helper.dart'; import '/utils/encryption/crypto_utils.dart'; -Future getConversationUser(Conversation conversation, String userId) async { - final db = await getDatabaseConnection(); - - final List> maps = await db.query( - 'conversation_users', - where: 'conversation_id = ? AND user_id = ?', - whereArgs: [ conversation.id, userId ], - ); - - if (maps.length != 1) { - throw ArgumentError('Invalid conversation_id or username'); - } - - return ConversationUser( - id: maps[0]['id'], - userId: maps[0]['user_id'], - conversationId: maps[0]['conversation_id'], - username: maps[0]['username'], - associationKey: maps[0]['association_key'], - publicKey: CryptoUtils.rsaPublicKeyFromPem(maps[0]['asymmetric_public_key']), - admin: maps[0]['admin'] == 1, - ); - -} - -// A method that retrieves all the dogs from the dogs table. -Future> getConversationUsers(Conversation conversation) async { - final db = await getDatabaseConnection(); - - final List> maps = await db.query( - 'conversation_users', - where: 'conversation_id = ?', - whereArgs: [ conversation.id ], - orderBy: 'username', - ); - - List conversationUsers = List.generate(maps.length, (i) { - return ConversationUser( - id: maps[i]['id'], - userId: maps[i]['user_id'], - conversationId: maps[i]['conversation_id'], - username: maps[i]['username'], - associationKey: maps[i]['association_key'], - publicKey: CryptoUtils.rsaPublicKeyFromPem(maps[i]['asymmetric_public_key']), - admin: maps[i]['admin'] == 1, - ); - }); - - int index = 0; - List finalConversationUsers = []; - - for (ConversationUser conversationUser in conversationUsers) { - if (!conversationUser.admin) { - finalConversationUsers.add(conversationUser); - continue; - } - - finalConversationUsers.insert(index, conversationUser); - index++; - } - - return finalConversationUsers; -} - -Future>> getEncryptedConversationUsers(Conversation conversation, Uint8List symKey) async { - final db = await getDatabaseConnection(); - - final List> maps = await db.query( - 'conversation_users', - where: 'conversation_id = ?', - whereArgs: [conversation.id], - orderBy: 'username', - ); - - List> conversationUsers = List.generate(maps.length, (i) { - return { - 'id': maps[i]['id'], - 'conversation_id': maps[i]['conversation_id'], - 'user_id': AesHelper.aesEncrypt(symKey, Uint8List.fromList(maps[i]['user_id'].codeUnits)), - 'username': AesHelper.aesEncrypt(symKey, Uint8List.fromList(maps[i]['username'].codeUnits)), - 'association_key': AesHelper.aesEncrypt(symKey, Uint8List.fromList(maps[i]['association_key'].codeUnits)), - 'public_key': AesHelper.aesEncrypt(symKey, Uint8List.fromList(maps[i]['asymmetric_public_key'].codeUnits)), - 'admin': AesHelper.aesEncrypt(symKey, Uint8List.fromList((maps[i]['admin'] == 1 ? 'true' : 'false').codeUnits)), - }; - }); - - return conversationUsers; -} - -class ConversationUser{ +class ConversationUser { String id; String userId; String conversationId; diff --git a/mobile/lib/database/models/conversations.dart b/mobile/lib/database/models/conversations.dart index f391743..f069ad5 100644 --- a/mobile/lib/database/models/conversations.dart +++ b/mobile/lib/database/models/conversations.dart @@ -2,11 +2,12 @@ import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; +import 'package:Envelope/database/repositories/friends_repository.dart'; import 'package:mime/mime.dart'; import 'package:pointycastle/export.dart'; -import 'package:sqflite/sqflite.dart'; import 'package:uuid/uuid.dart'; +import '/database/repositories/conversation_users_repository.dart'; import '/database/models/messages.dart'; import '/database/models/text_messages.dart'; import '/database/models/conversation_users.dart'; @@ -15,198 +16,12 @@ import '/database/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'; import '/utils/storage/write_file.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', - adminAddMembers: true, - adminEditInfo: true, - adminSendMessages: false, - ); - - 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 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'], - adminAddMembers: maps[0]['admin_add_members'] == 1, - adminEditInfo: maps[0]['admin_edit_info'] == 1, - adminSendMessages: maps[0]['admin_send_messages'] == 1, - ); -} - -// 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', - adminAddMembers: maps[i]['admin_add_members'] == 1, - adminEditInfo: maps[i]['admin_edit_info'] == 1, - adminSendMessages: maps[i]['admin_send_messages'] == 1, - ); - }); -} - -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'], - adminAddMembers: maps[0]['admin_add_members'] == 1, - adminEditInfo: maps[0]['admin_edit_info'] == 1, - adminSendMessages: maps[0]['admin_send_messages'] == 1, - ); - +enum ConversationStatus { + complete, + pending, + error, } class Conversation { @@ -282,11 +97,11 @@ class Conversation { return { 'id': id, 'name': AesHelper.aesEncrypt(symKey, Uint8List.fromList(name.codeUnits)), - 'users': await getEncryptedConversationUsers(this, symKey), + 'users': await ConversationUsersRepository.getEncryptedConversationUsers(this, symKey), }; } - List users = await getConversationUsers(this); + List users = await ConversationUsersRepository.getConversationUsers(this); List userConversations = []; @@ -296,7 +111,7 @@ class Conversation { String newId = id; if (profile.id != user.userId) { - Friend friend = await getFriendByFriendId(user.userId); + Friend friend = await FriendsRepository.getFriendByFriendId(user.userId); pubKey = friend.publicKey; newId = (const Uuid()).v4(); } @@ -313,7 +128,7 @@ class Conversation { Map returnData = { 'id': id, 'name': AesHelper.aesEncrypt(symKey, Uint8List.fromList(name.codeUnits)), - 'users': await getEncryptedConversationUsers(this, symKey), + 'users': await ConversationUsersRepository.getEncryptedConversationUsers(this, symKey), 'two_user': AesHelper.aesEncrypt(symKey, Uint8List.fromList((twoUser ? 'true' : 'false').codeUnits)), 'message_expiry': messageExpiryDefault, 'admin_add_members': AesHelper.aesEncrypt(symKey, Uint8List.fromList((adminAddMembers ? 'true' : 'false').codeUnits)), @@ -405,10 +220,3 @@ class Conversation { ); } } - - -enum ConversationStatus { - complete, - pending, - error, -} diff --git a/mobile/lib/database/models/friends.dart b/mobile/lib/database/models/friends.dart index c435697..d696c0d 100644 --- a/mobile/lib/database/models/friends.dart +++ b/mobile/lib/database/models/friends.dart @@ -7,6 +7,7 @@ import '/utils/encryption/aes_helper.dart'; import '/utils/encryption/crypto_utils.dart'; import '/utils/storage/database.dart'; +// TODO: Find good place for this to live Friend findFriendByFriendId(List friends, String id) { for (var friend in friends) { if (friend.friendId == id) { @@ -17,61 +18,6 @@ Friend findFriendByFriendId(List friends, String id) { throw ArgumentError.value(id, 'id', 'No element with that id'); } -Future getFriendByFriendId(String userId) async { - final db = await getDatabaseConnection(); - - final List> maps = await db.query( - 'friends', - where: 'friend_id = ?', - whereArgs: [userId], - ); - - if (maps.length != 1) { - throw ArgumentError('Invalid user id'); - } - - return Friend( - id: maps[0]['id'], - userId: maps[0]['user_id'], - friendId: maps[0]['friend_id'], - friendSymmetricKey: maps[0]['symmetric_key'], - publicKey: CryptoUtils.rsaPublicKeyFromPem(maps[0]['asymmetric_public_key']), - acceptedAt: maps[0]['accepted_at'] != null ? DateTime.parse(maps[0]['accepted_at']) : null, - username: maps[0]['username'], - ); -} - - -Future> getFriends({bool? accepted}) async { - final db = await getDatabaseConnection(); - - String? where; - - if (accepted == true) { - where = 'accepted_at IS NOT NULL'; - } - - if (accepted == false) { - where = 'accepted_at IS NULL'; - } - - final List> maps = await db.query( - 'friends', - where: where, - ); - - return List.generate(maps.length, (i) { - return Friend( - id: maps[i]['id'], - userId: maps[i]['user_id'], - friendId: maps[i]['friend_id'], - friendSymmetricKey: maps[i]['symmetric_key'], - publicKey: CryptoUtils.rsaPublicKeyFromPem(maps[i]['asymmetric_public_key']), - acceptedAt: maps[i]['accepted_at'] != null ? DateTime.parse(maps[i]['accepted_at']) : null, - username: maps[i]['username'], - ); - }); -} class Friend{ String id; diff --git a/mobile/lib/database/models/messages.dart b/mobile/lib/database/models/messages.dart index 371d407..03bdab2 100644 --- a/mobile/lib/database/models/messages.dart +++ b/mobile/lib/database/models/messages.dart @@ -1,66 +1,17 @@ import 'dart:convert'; -import 'dart:io'; import 'dart:typed_data'; +import 'package:Envelope/database/repositories/conversation_users_repository.dart'; import 'package:pointycastle/pointycastle.dart'; -import 'package:uuid/uuid.dart'; -import '/database/models/image_message.dart'; import '/database/models/conversation_users.dart'; import '/database/models/my_profile.dart'; -import '/database/models/text_messages.dart'; import '/database/models/conversations.dart'; import '/utils/encryption/crypto_utils.dart'; -import '/utils/storage/database.dart'; const messageTypeReceiver = 'receiver'; const messageTypeSender = 'sender'; -Future> getMessagesForThread(Conversation conversation) 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; - ''', - [conversation.id] - ); - - return List.generate(maps.length, (i) { - if (maps[i]['data'] == null) { - - File file = File(maps[i]['file']); - - return ImageMessage( - id: maps[i]['id'], - symmetricKey: maps[i]['symmetric_key'], - userSymmetricKey: maps[i]['user_symmetric_key'], - file: file, - senderId: maps[i]['sender_id'], - senderUsername: maps[i]['sender_username'], - associationKey: maps[i]['association_key'], - createdAt: maps[i]['created_at'], - failedToSend: maps[i]['failed_to_send'] == 1, - ); - } - - return TextMessage( - id: maps[i]['id'], - symmetricKey: maps[i]['symmetric_key'], - userSymmetricKey: maps[i]['user_symmetric_key'], - text: maps[i]['data'], - senderId: maps[i]['sender_id'], - senderUsername: maps[i]['sender_username'], - associationKey: maps[i]['association_key'], - createdAt: maps[i]['created_at'], - failedToSend: maps[i]['failed_to_send'] == 1, - ); - }); -} - class Message { String id; String symmetricKey; @@ -97,7 +48,7 @@ class Message { RSAPublicKey publicKey = profile.publicKey!; List> messages = []; - List conversationUsers = await getConversationUsers(conversation); + List conversationUsers = await ConversationUsersRepository.getConversationUsers(conversation); for (var i = 0; i < conversationUsers.length; i++) { ConversationUser user = conversationUsers[i]; @@ -116,7 +67,7 @@ class Message { continue; } - ConversationUser conversationUser = await getConversationUser(conversation, user.userId); + ConversationUser conversationUser = await ConversationUsersRepository.getConversationUser(conversation, user.userId); RSAPublicKey friendPublicKey = conversationUser.publicKey; messages.add({ diff --git a/mobile/lib/database/repositories/conversation_users_repository.dart b/mobile/lib/database/repositories/conversation_users_repository.dart new file mode 100644 index 0000000..c4819cc --- /dev/null +++ b/mobile/lib/database/repositories/conversation_users_repository.dart @@ -0,0 +1,97 @@ +import 'dart:typed_data'; + +import '/database/models/conversation_users.dart'; +import '/database/models/conversations.dart'; +import '/utils/storage/database.dart'; +import '/utils/encryption/aes_helper.dart'; +import '/utils/encryption/crypto_utils.dart'; + +class ConversationUsersRepository { + + static Future getConversationUser(Conversation conversation, String userId) async { + final db = await getDatabaseConnection(); + + final List> maps = await db.query( + 'conversation_users', + where: 'conversation_id = ? AND user_id = ?', + whereArgs: [ conversation.id, userId ], + ); + + if (maps.length != 1) { + throw ArgumentError('Invalid conversation_id or username'); + } + + return ConversationUser( + id: maps[0]['id'], + userId: maps[0]['user_id'], + conversationId: maps[0]['conversation_id'], + username: maps[0]['username'], + associationKey: maps[0]['association_key'], + publicKey: CryptoUtils.rsaPublicKeyFromPem(maps[0]['asymmetric_public_key']), + admin: maps[0]['admin'] == 1, + ); + } + + static Future> getConversationUsers(Conversation conversation) async { + final db = await getDatabaseConnection(); + + final List> maps = await db.query( + 'conversation_users', + where: 'conversation_id = ?', + whereArgs: [ conversation.id ], + orderBy: 'username', + ); + + List conversationUsers = List.generate(maps.length, (i) { + return ConversationUser( + id: maps[i]['id'], + userId: maps[i]['user_id'], + conversationId: maps[i]['conversation_id'], + username: maps[i]['username'], + associationKey: maps[i]['association_key'], + publicKey: CryptoUtils.rsaPublicKeyFromPem(maps[i]['asymmetric_public_key']), + admin: maps[i]['admin'] == 1, + ); + }); + + int index = 0; + List finalConversationUsers = []; + + for (ConversationUser conversationUser in conversationUsers) { + if (!conversationUser.admin) { + finalConversationUsers.add(conversationUser); + continue; + } + + finalConversationUsers.insert(index, conversationUser); + index++; + } + + return finalConversationUsers; + } + + static Future>> getEncryptedConversationUsers(Conversation conversation, Uint8List symKey) async { + final db = await getDatabaseConnection(); + + final List> maps = await db.query( + 'conversation_users', + where: 'conversation_id = ?', + whereArgs: [conversation.id], + orderBy: 'username', + ); + + List> conversationUsers = List.generate(maps.length, (i) { + return { + 'id': maps[i]['id'], + 'conversation_id': maps[i]['conversation_id'], + 'user_id': AesHelper.aesEncrypt(symKey, Uint8List.fromList(maps[i]['user_id'].codeUnits)), + 'username': AesHelper.aesEncrypt(symKey, Uint8List.fromList(maps[i]['username'].codeUnits)), + 'association_key': AesHelper.aesEncrypt(symKey, Uint8List.fromList(maps[i]['association_key'].codeUnits)), + 'public_key': AesHelper.aesEncrypt(symKey, Uint8List.fromList(maps[i]['asymmetric_public_key'].codeUnits)), + 'admin': AesHelper.aesEncrypt(symKey, Uint8List.fromList((maps[i]['admin'] == 1 ? 'true' : 'false').codeUnits)), + }; + }); + + return conversationUsers; + } +} diff --git a/mobile/lib/database/repositories/conversations_repository.dart b/mobile/lib/database/repositories/conversations_repository.dart new file mode 100644 index 0000000..93a3587 --- /dev/null +++ b/mobile/lib/database/repositories/conversations_repository.dart @@ -0,0 +1,206 @@ +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:Envelope/database/models/conversation_users.dart'; +import 'package:sqflite/sql.dart'; +import 'package:uuid/uuid.dart'; + +import '/database/models/conversations.dart'; +import '/database/models/friends.dart'; +import '/database/models/my_profile.dart'; +import '/utils/encryption/aes_helper.dart'; +import '/utils/strings.dart'; +import '/utils/storage/database.dart'; + +class ConversationsRepository { + + static 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', + adminAddMembers: true, + adminEditInfo: true, + adminSendMessages: false, + ); + + 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; + } + + static 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'], + adminAddMembers: maps[0]['admin_add_members'] == 1, + adminEditInfo: maps[0]['admin_edit_info'] == 1, + adminSendMessages: maps[0]['admin_send_messages'] == 1, + ); + } + + static 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', + adminAddMembers: maps[i]['admin_add_members'] == 1, + adminEditInfo: maps[i]['admin_edit_info'] == 1, + adminSendMessages: maps[i]['admin_send_messages'] == 1, + ); + }); + } + + static 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'], + adminAddMembers: maps[0]['admin_add_members'] == 1, + adminEditInfo: maps[0]['admin_edit_info'] == 1, + adminSendMessages: maps[0]['admin_send_messages'] == 1, + ); + } +} + diff --git a/mobile/lib/database/repositories/friends_repository.dart b/mobile/lib/database/repositories/friends_repository.dart new file mode 100644 index 0000000..c56dd28 --- /dev/null +++ b/mobile/lib/database/repositories/friends_repository.dart @@ -0,0 +1,62 @@ + +import '/database/models/friends.dart'; +import '/utils/encryption/crypto_utils.dart'; +import '/utils/storage/database.dart'; + +class FriendsRepository { + + static Future getFriendByFriendId(String userId) async { + final db = await getDatabaseConnection(); + + final List> maps = await db.query( + 'friends', + where: 'friend_id = ?', + whereArgs: [userId], + ); + + if (maps.length != 1) { + throw ArgumentError('Invalid user id'); + } + + return Friend( + id: maps[0]['id'], + userId: maps[0]['user_id'], + friendId: maps[0]['friend_id'], + friendSymmetricKey: maps[0]['symmetric_key'], + publicKey: CryptoUtils.rsaPublicKeyFromPem(maps[0]['asymmetric_public_key']), + acceptedAt: maps[0]['accepted_at'] != null ? DateTime.parse(maps[0]['accepted_at']) : null, + username: maps[0]['username'], + ); + } + + static Future> getFriends({bool? accepted}) async { + final db = await getDatabaseConnection(); + + String? where; + + if (accepted == true) { + where = 'accepted_at IS NOT NULL'; + } + + if (accepted == false) { + where = 'accepted_at IS NULL'; + } + + final List> maps = await db.query( + 'friends', + where: where, + ); + + return List.generate(maps.length, (i) { + return Friend( + id: maps[i]['id'], + userId: maps[i]['user_id'], + friendId: maps[i]['friend_id'], + friendSymmetricKey: maps[i]['symmetric_key'], + publicKey: CryptoUtils.rsaPublicKeyFromPem(maps[i]['asymmetric_public_key']), + acceptedAt: maps[i]['accepted_at'] != null ? DateTime.parse(maps[i]['accepted_at']) : null, + username: maps[i]['username'], + ); + }); + } +} diff --git a/mobile/lib/database/repositories/messages_repository.dart b/mobile/lib/database/repositories/messages_repository.dart new file mode 100644 index 0000000..16b5af3 --- /dev/null +++ b/mobile/lib/database/repositories/messages_repository.dart @@ -0,0 +1,54 @@ +import 'dart:io'; + +import '/database/models/conversations.dart'; +import '/database/models/image_message.dart'; +import '/database/models/messages.dart'; +import '/database/models/text_messages.dart'; +import '/utils/storage/database.dart'; + +class MessagesRepository { + static Future> getMessagesForThread(Conversation conversation) 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; + ''', + [conversation.id] + ); + + return List.generate(maps.length, (i) { + if (maps[i]['data'] == null) { + + File file = File(maps[i]['file']); + + return ImageMessage( + id: maps[i]['id'], + symmetricKey: maps[i]['symmetric_key'], + userSymmetricKey: maps[i]['user_symmetric_key'], + file: file, + senderId: maps[i]['sender_id'], + senderUsername: maps[i]['sender_username'], + associationKey: maps[i]['association_key'], + createdAt: maps[i]['created_at'], + failedToSend: maps[i]['failed_to_send'] == 1, + ); + } + + return TextMessage( + id: maps[i]['id'], + symmetricKey: maps[i]['symmetric_key'], + userSymmetricKey: maps[i]['user_symmetric_key'], + text: maps[i]['data'], + senderId: maps[i]['sender_id'], + senderUsername: maps[i]['sender_username'], + associationKey: maps[i]['association_key'], + createdAt: maps[i]['created_at'], + failedToSend: maps[i]['failed_to_send'] == 1, + ); + }); + } +} diff --git a/mobile/lib/services/messages_service.dart b/mobile/lib/services/messages_service.dart index cf331f2..1c5f559 100644 --- a/mobile/lib/services/messages_service.dart +++ b/mobile/lib/services/messages_service.dart @@ -1,6 +1,8 @@ import 'dart:convert'; import 'dart:io'; +import 'package:Envelope/database/repositories/conversation_users_repository.dart'; +import 'package:Envelope/database/repositories/conversations_repository.dart'; import 'package:http/http.dart' as http; import 'package:sqflite/sqflite.dart'; import 'package:uuid/uuid.dart'; @@ -26,7 +28,7 @@ class MessagesService { var uuid = const Uuid(); - ConversationUser currentUser = await getConversationUser(conversation, profile.id); + ConversationUser currentUser = await ConversationUsersRepository.getConversationUser(conversation, profile.id); List messages = []; List> messagesToSend = []; @@ -122,7 +124,7 @@ class MessagesService { static Future updateMessageThread(Conversation conversation, {MyProfile? profile}) async { profile ??= await MyProfile.getProfile(); - ConversationUser currentUser = await getConversationUser(conversation, profile.id); + ConversationUser currentUser = await ConversationUsersRepository.getConversationUser(conversation, profile.id); var resp = await http.get( await MyProfile.getServerUrl('api/v1/auth/messages/${currentUser.associationKey}'), @@ -152,7 +154,7 @@ class MessagesService { profile.privateKey!, ); - ConversationUser messageUser = await getConversationUser(conversation, message.senderId); + ConversationUser messageUser = await ConversationUsersRepository.getConversationUser(conversation, message.senderId); message.senderUsername = messageUser.username; await db.insert( @@ -166,7 +168,7 @@ class MessagesService { static Future updateMessageThreads({List? conversations}) async { MyProfile profile = await MyProfile.getProfile(); - conversations ??= await getConversations(); + conversations ??= await ConversationsRepository.getConversations(); for (var i = 0; i < conversations.length; i++) { await updateMessageThread(conversations[i], profile: profile); diff --git a/mobile/lib/views/main/conversation/detail.dart b/mobile/lib/views/main/conversation/detail.dart index 3fa1ec0..fe09c29 100644 --- a/mobile/lib/views/main/conversation/detail.dart +++ b/mobile/lib/views/main/conversation/detail.dart @@ -1,5 +1,6 @@ import 'dart:io'; +import 'package:Envelope/database/repositories/messages_repository.dart'; import 'package:Envelope/services/messages_service.dart'; import 'package:Envelope/views/main/conversation/message.dart'; import 'package:flutter/material.dart'; @@ -78,7 +79,7 @@ class _ConversationDetailState extends State { Future fetchMessages() async { profile = await MyProfile.getProfile(); - messages = await getMessagesForThread(widget.conversation); + messages = await MessagesRepository.getMessagesForThread(widget.conversation); setState(() {}); } @@ -263,7 +264,7 @@ class _ConversationDetailState extends State { data: msgController.text != '' ? msgController.text : null, files: selectedImages, ); - messages = await getMessagesForThread(widget.conversation); + messages = await MessagesRepository.getMessagesForThread(widget.conversation); setState(() { msgController.text = ''; selectedImages = []; diff --git a/mobile/lib/views/main/conversation/list.dart b/mobile/lib/views/main/conversation/list.dart index a82067b..59ce4b1 100644 --- a/mobile/lib/views/main/conversation/list.dart +++ b/mobile/lib/views/main/conversation/list.dart @@ -2,6 +2,8 @@ import 'dart:io'; import 'package:Envelope/components/custom_title_bar.dart'; import 'package:Envelope/components/flash_message.dart'; +import 'package:Envelope/database/repositories/conversations_repository.dart'; +import 'package:Envelope/database/repositories/friends_repository.dart'; import 'package:Envelope/services/conversations_service.dart'; import 'package:flutter/material.dart'; @@ -74,7 +76,7 @@ class _ConversationListState extends State { MaterialPageRoute(builder: (context) => ConversationAddFriendsList( friends: friends, saveCallback: (List friendsSelected) async { - Conversation conversation = await createConversation( + Conversation conversation = await ConversationsRepository.createConversation( conversationName, friendsSelected, false, @@ -166,8 +168,8 @@ class _ConversationListState extends State { } onGoBack(dynamic value) async { - conversations = await getConversations(); - friends = await getFriends(); + conversations = await ConversationsRepository.getConversations(); + friends = await FriendsRepository.getFriends(); setState(() {}); } } diff --git a/mobile/lib/views/main/conversation/list_item.dart b/mobile/lib/views/main/conversation/list_item.dart index ecd3f9a..357158f 100644 --- a/mobile/lib/views/main/conversation/list_item.dart +++ b/mobile/lib/views/main/conversation/list_item.dart @@ -1,3 +1,4 @@ +import 'package:Envelope/database/repositories/conversations_repository.dart'; import 'package:flutter/material.dart'; import '/database/models/messages.dart'; @@ -113,7 +114,7 @@ class _ConversationListItemState extends State { } onGoBack(dynamic value) async { - conversation = await getConversationById(widget.conversation.id); + conversation = await ConversationsRepository.getConversationById(widget.conversation.id); setState(() {}); } } diff --git a/mobile/lib/views/main/conversation/settings.dart b/mobile/lib/views/main/conversation/settings.dart index 2920359..866de8f 100644 --- a/mobile/lib/views/main/conversation/settings.dart +++ b/mobile/lib/views/main/conversation/settings.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'dart:io'; +import 'package:Envelope/database/repositories/conversation_users_repository.dart'; import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; @@ -156,7 +157,7 @@ class _ConversationSettingsState extends State { } Future getUsers() async { - users = await getConversationUsers(widget.conversation); + users = await ConversationUsersRepository.getConversationUsers(widget.conversation); profile = await MyProfile.getProfile(); setState(() {}); } diff --git a/mobile/lib/views/main/friend/list.dart b/mobile/lib/views/main/friend/list.dart index 73ec21b..c7c6219 100644 --- a/mobile/lib/views/main/friend/list.dart +++ b/mobile/lib/views/main/friend/list.dart @@ -1,3 +1,4 @@ +import 'package:Envelope/database/repositories/friends_repository.dart'; import 'package:flutter/material.dart'; import '/components/custom_title_bar.dart'; @@ -148,8 +149,8 @@ class _FriendListState extends State { } Future initFriends() async { - friends = await getFriends(accepted: true); - friendRequests = await getFriends(accepted: false); + friends = await FriendsRepository.getFriends(accepted: true); + friendRequests = await FriendsRepository.getFriends(accepted: false); setState(() {}); widget.callback(); } diff --git a/mobile/lib/views/main/friend/list_item.dart b/mobile/lib/views/main/friend/list_item.dart index d8d9076..d140ac7 100644 --- a/mobile/lib/views/main/friend/list_item.dart +++ b/mobile/lib/views/main/friend/list_item.dart @@ -1,3 +1,4 @@ +import 'package:Envelope/database/repositories/conversations_repository.dart'; import 'package:flutter/material.dart'; import '/components/custom_circle_avatar.dart'; @@ -61,9 +62,9 @@ class _FriendListItemState extends State { } Future findOrCreateConversation(BuildContext context) async { - Conversation? conversation = await getTwoUserConversation(widget.friend.friendId); + Conversation? conversation = await ConversationsRepository.getTwoUserConversation(widget.friend.friendId); - conversation ??= await createConversation( + conversation ??= await ConversationsRepository.createConversation( generateRandomString(32), [ widget.friend ], true, diff --git a/mobile/lib/views/main/home.dart b/mobile/lib/views/main/home.dart index 857b859..087d159 100644 --- a/mobile/lib/views/main/home.dart +++ b/mobile/lib/views/main/home.dart @@ -1,3 +1,5 @@ +import 'package:Envelope/database/repositories/conversations_repository.dart'; +import 'package:Envelope/database/repositories/friends_repository.dart'; import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; @@ -157,9 +159,9 @@ class _HomeState extends State { await ConversationsService.updateConversations(); await MessagesService.updateMessageThreads(); - conversations = await getConversations(); - friends = await getFriends(accepted: true); - friendRequests = await getFriends(accepted: false); + conversations = await ConversationsRepository.getConversations(); + friends = await FriendsRepository.getFriends(accepted: true); + friendRequests = await FriendsRepository.getFriends(accepted: false); profile = await MyProfile.getProfile(); setState(() { @@ -181,9 +183,9 @@ class _HomeState extends State { Future reinitDatabaseRecords() async { - conversations = await getConversations(); - friends = await getFriends(accepted: true); - friendRequests = await getFriends(accepted: false); + conversations = await ConversationsRepository.getConversations(); + friends = await FriendsRepository.getFriends(accepted: true); + friendRequests = await FriendsRepository.getFriends(accepted: false); profile = await MyProfile.getProfile(); setState(() {