Browse Source

Add repository classes to interact with DB

feature/add-notifications
Tovi Jaeschke-Rogers 2 years ago
parent
commit
8b0ab86c83
16 changed files with 466 additions and 422 deletions
  1. +1
    -92
      mobile/lib/database/models/conversation_users.dart
  2. +10
    -202
      mobile/lib/database/models/conversations.dart
  3. +1
    -55
      mobile/lib/database/models/friends.dart
  4. +3
    -52
      mobile/lib/database/models/messages.dart
  5. +97
    -0
      mobile/lib/database/repositories/conversation_users_repository.dart
  6. +206
    -0
      mobile/lib/database/repositories/conversations_repository.dart
  7. +62
    -0
      mobile/lib/database/repositories/friends_repository.dart
  8. +54
    -0
      mobile/lib/database/repositories/messages_repository.dart
  9. +6
    -4
      mobile/lib/services/messages_service.dart
  10. +3
    -2
      mobile/lib/views/main/conversation/detail.dart
  11. +5
    -3
      mobile/lib/views/main/conversation/list.dart
  12. +2
    -1
      mobile/lib/views/main/conversation/list_item.dart
  13. +2
    -1
      mobile/lib/views/main/conversation/settings.dart
  14. +3
    -2
      mobile/lib/views/main/friend/list.dart
  15. +3
    -2
      mobile/lib/views/main/friend/list_item.dart
  16. +8
    -6
      mobile/lib/views/main/home.dart

+ 1
- 92
mobile/lib/database/models/conversation_users.dart View File

@ -3,101 +3,10 @@ import 'dart:typed_data';
import 'package:pointycastle/impl.dart'; import 'package:pointycastle/impl.dart';
import '/database/models/conversations.dart';
import '/utils/storage/database.dart';
import '/utils/encryption/aes_helper.dart'; import '/utils/encryption/aes_helper.dart';
import '/utils/encryption/crypto_utils.dart'; import '/utils/encryption/crypto_utils.dart';
Future<ConversationUser> getConversationUser(Conversation conversation, String userId) async {
final db = await getDatabaseConnection();
final List<Map<String, dynamic>> 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<List<ConversationUser>> getConversationUsers(Conversation conversation) async {
final db = await getDatabaseConnection();
final List<Map<String, dynamic>> maps = await db.query(
'conversation_users',
where: 'conversation_id = ?',
whereArgs: [ conversation.id ],
orderBy: 'username',
);
List<ConversationUser> 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<ConversationUser> finalConversationUsers = [];
for (ConversationUser conversationUser in conversationUsers) {
if (!conversationUser.admin) {
finalConversationUsers.add(conversationUser);
continue;
}
finalConversationUsers.insert(index, conversationUser);
index++;
}
return finalConversationUsers;
}
Future<List<Map<String, dynamic>>> getEncryptedConversationUsers(Conversation conversation, Uint8List symKey) async {
final db = await getDatabaseConnection();
final List<Map<String, dynamic>> maps = await db.query(
'conversation_users',
where: 'conversation_id = ?',
whereArgs: [conversation.id],
orderBy: 'username',
);
List<Map<String, dynamic>> 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 id;
String userId; String userId;
String conversationId; String conversationId;


+ 10
- 202
mobile/lib/database/models/conversations.dart View File

@ -2,11 +2,12 @@ import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:Envelope/database/repositories/friends_repository.dart';
import 'package:mime/mime.dart'; import 'package:mime/mime.dart';
import 'package:pointycastle/export.dart'; import 'package:pointycastle/export.dart';
import 'package:sqflite/sqflite.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
import '/database/repositories/conversation_users_repository.dart';
import '/database/models/messages.dart'; import '/database/models/messages.dart';
import '/database/models/text_messages.dart'; import '/database/models/text_messages.dart';
import '/database/models/conversation_users.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/aes_helper.dart';
import '/utils/encryption/crypto_utils.dart'; import '/utils/encryption/crypto_utils.dart';
import '/utils/storage/database.dart'; import '/utils/storage/database.dart';
import '/utils/strings.dart';
import '/utils/storage/write_file.dart'; import '/utils/storage/write_file.dart';
Future<Conversation> createConversation(String title, List<Friend> 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<Map<String, dynamic>> 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<Conversation> getConversationById(String id) async {
final db = await getDatabaseConnection();
final List<Map<String, dynamic>> 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<List<Conversation>> getConversations() async {
final db = await getDatabaseConnection();
final List<Map<String, dynamic>> 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<Conversation?> getTwoUserConversation(String userId) async {
final db = await getDatabaseConnection();
MyProfile profile = await MyProfile.getProfile();
final List<Map<String, dynamic>> 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 { class Conversation {
@ -282,11 +97,11 @@ class Conversation {
return { return {
'id': id, 'id': id,
'name': AesHelper.aesEncrypt(symKey, Uint8List.fromList(name.codeUnits)), 'name': AesHelper.aesEncrypt(symKey, Uint8List.fromList(name.codeUnits)),
'users': await getEncryptedConversationUsers(this, symKey),
'users': await ConversationUsersRepository.getEncryptedConversationUsers(this, symKey),
}; };
} }
List<ConversationUser> users = await getConversationUsers(this);
List<ConversationUser> users = await ConversationUsersRepository.getConversationUsers(this);
List<Object> userConversations = []; List<Object> userConversations = [];
@ -296,7 +111,7 @@ class Conversation {
String newId = id; String newId = id;
if (profile.id != user.userId) { if (profile.id != user.userId) {
Friend friend = await getFriendByFriendId(user.userId);
Friend friend = await FriendsRepository.getFriendByFriendId(user.userId);
pubKey = friend.publicKey; pubKey = friend.publicKey;
newId = (const Uuid()).v4(); newId = (const Uuid()).v4();
} }
@ -313,7 +128,7 @@ class Conversation {
Map<String, dynamic> returnData = { Map<String, dynamic> returnData = {
'id': id, 'id': id,
'name': AesHelper.aesEncrypt(symKey, Uint8List.fromList(name.codeUnits)), '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)), 'two_user': AesHelper.aesEncrypt(symKey, Uint8List.fromList((twoUser ? 'true' : 'false').codeUnits)),
'message_expiry': messageExpiryDefault, 'message_expiry': messageExpiryDefault,
'admin_add_members': AesHelper.aesEncrypt(symKey, Uint8List.fromList((adminAddMembers ? 'true' : 'false').codeUnits)), 'admin_add_members': AesHelper.aesEncrypt(symKey, Uint8List.fromList((adminAddMembers ? 'true' : 'false').codeUnits)),
@ -405,10 +220,3 @@ class Conversation {
); );
} }
} }
enum ConversationStatus {
complete,
pending,
error,
}

+ 1
- 55
mobile/lib/database/models/friends.dart View File

@ -7,6 +7,7 @@ import '/utils/encryption/aes_helper.dart';
import '/utils/encryption/crypto_utils.dart'; import '/utils/encryption/crypto_utils.dart';
import '/utils/storage/database.dart'; import '/utils/storage/database.dart';
// TODO: Find good place for this to live
Friend findFriendByFriendId(List<Friend> friends, String id) { Friend findFriendByFriendId(List<Friend> friends, String id) {
for (var friend in friends) { for (var friend in friends) {
if (friend.friendId == id) { if (friend.friendId == id) {
@ -17,61 +18,6 @@ Friend findFriendByFriendId(List<Friend> friends, String id) {
throw ArgumentError.value(id, 'id', 'No element with that id'); throw ArgumentError.value(id, 'id', 'No element with that id');
} }
Future<Friend> getFriendByFriendId(String userId) async {
final db = await getDatabaseConnection();
final List<Map<String, dynamic>> 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<List<Friend>> 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<Map<String, dynamic>> 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{ class Friend{
String id; String id;


+ 3
- 52
mobile/lib/database/models/messages.dart View File

@ -1,66 +1,17 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:io';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:Envelope/database/repositories/conversation_users_repository.dart';
import 'package:pointycastle/pointycastle.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/conversation_users.dart';
import '/database/models/my_profile.dart'; import '/database/models/my_profile.dart';
import '/database/models/text_messages.dart';
import '/database/models/conversations.dart'; import '/database/models/conversations.dart';
import '/utils/encryption/crypto_utils.dart'; import '/utils/encryption/crypto_utils.dart';
import '/utils/storage/database.dart';
const messageTypeReceiver = 'receiver'; const messageTypeReceiver = 'receiver';
const messageTypeSender = 'sender'; const messageTypeSender = 'sender';
Future<List<Message>> getMessagesForThread(Conversation conversation) async {
final db = await getDatabaseConnection();
final List<Map<String, dynamic>> 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 { class Message {
String id; String id;
String symmetricKey; String symmetricKey;
@ -97,7 +48,7 @@ class Message {
RSAPublicKey publicKey = profile.publicKey!; RSAPublicKey publicKey = profile.publicKey!;
List<Map<String, String>> messages = []; List<Map<String, String>> messages = [];
List<ConversationUser> conversationUsers = await getConversationUsers(conversation);
List<ConversationUser> conversationUsers = await ConversationUsersRepository.getConversationUsers(conversation);
for (var i = 0; i < conversationUsers.length; i++) { for (var i = 0; i < conversationUsers.length; i++) {
ConversationUser user = conversationUsers[i]; ConversationUser user = conversationUsers[i];
@ -116,7 +67,7 @@ class Message {
continue; continue;
} }
ConversationUser conversationUser = await getConversationUser(conversation, user.userId);
ConversationUser conversationUser = await ConversationUsersRepository.getConversationUser(conversation, user.userId);
RSAPublicKey friendPublicKey = conversationUser.publicKey; RSAPublicKey friendPublicKey = conversationUser.publicKey;
messages.add({ messages.add({


+ 97
- 0
mobile/lib/database/repositories/conversation_users_repository.dart View File

@ -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<ConversationUser> getConversationUser(Conversation conversation, String userId) async {
final db = await getDatabaseConnection();
final List<Map<String, dynamic>> 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<List<ConversationUser>> getConversationUsers(Conversation conversation) async {
final db = await getDatabaseConnection();
final List<Map<String, dynamic>> maps = await db.query(
'conversation_users',
where: 'conversation_id = ?',
whereArgs: [ conversation.id ],
orderBy: 'username',
);
List<ConversationUser> 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<ConversationUser> finalConversationUsers = [];
for (ConversationUser conversationUser in conversationUsers) {
if (!conversationUser.admin) {
finalConversationUsers.add(conversationUser);
continue;
}
finalConversationUsers.insert(index, conversationUser);
index++;
}
return finalConversationUsers;
}
static Future<List<Map<String, dynamic>>> getEncryptedConversationUsers(Conversation conversation, Uint8List symKey) async {
final db = await getDatabaseConnection();
final List<Map<String, dynamic>> maps = await db.query(
'conversation_users',
where: 'conversation_id = ?',
whereArgs: [conversation.id],
orderBy: 'username',
);
List<Map<String, dynamic>> 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;
}
}

+ 206
- 0
mobile/lib/database/repositories/conversations_repository.dart View File

@ -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<Conversation> createConversation(String title, List<Friend> 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<Map<String, dynamic>> 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<Conversation> getConversationById(String id) async {
final db = await getDatabaseConnection();
final List<Map<String, dynamic>> 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<List<Conversation>> getConversations() async {
final db = await getDatabaseConnection();
final List<Map<String, dynamic>> 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<Conversation?> getTwoUserConversation(String userId) async {
final db = await getDatabaseConnection();
MyProfile profile = await MyProfile.getProfile();
final List<Map<String, dynamic>> 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,
);
}
}

+ 62
- 0
mobile/lib/database/repositories/friends_repository.dart View File

@ -0,0 +1,62 @@
import '/database/models/friends.dart';
import '/utils/encryption/crypto_utils.dart';
import '/utils/storage/database.dart';
class FriendsRepository {
static Future<Friend> getFriendByFriendId(String userId) async {
final db = await getDatabaseConnection();
final List<Map<String, dynamic>> 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<List<Friend>> 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<Map<String, dynamic>> 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'],
);
});
}
}

+ 54
- 0
mobile/lib/database/repositories/messages_repository.dart View File

@ -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<List<Message>> getMessagesForThread(Conversation conversation) async {
final db = await getDatabaseConnection();
final List<Map<String, dynamic>> 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,
);
});
}
}

+ 6
- 4
mobile/lib/services/messages_service.dart View File

@ -1,6 +1,8 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; 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:http/http.dart' as http;
import 'package:sqflite/sqflite.dart'; import 'package:sqflite/sqflite.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
@ -26,7 +28,7 @@ class MessagesService {
var uuid = const Uuid(); var uuid = const Uuid();
ConversationUser currentUser = await getConversationUser(conversation, profile.id);
ConversationUser currentUser = await ConversationUsersRepository.getConversationUser(conversation, profile.id);
List<Message> messages = []; List<Message> messages = [];
List<Map<String, dynamic>> messagesToSend = []; List<Map<String, dynamic>> messagesToSend = [];
@ -122,7 +124,7 @@ class MessagesService {
static Future<void> updateMessageThread(Conversation conversation, {MyProfile? profile}) async { static Future<void> updateMessageThread(Conversation conversation, {MyProfile? profile}) async {
profile ??= await MyProfile.getProfile(); profile ??= await MyProfile.getProfile();
ConversationUser currentUser = await getConversationUser(conversation, profile.id);
ConversationUser currentUser = await ConversationUsersRepository.getConversationUser(conversation, profile.id);
var resp = await http.get( var resp = await http.get(
await MyProfile.getServerUrl('api/v1/auth/messages/${currentUser.associationKey}'), await MyProfile.getServerUrl('api/v1/auth/messages/${currentUser.associationKey}'),
@ -152,7 +154,7 @@ class MessagesService {
profile.privateKey!, profile.privateKey!,
); );
ConversationUser messageUser = await getConversationUser(conversation, message.senderId);
ConversationUser messageUser = await ConversationUsersRepository.getConversationUser(conversation, message.senderId);
message.senderUsername = messageUser.username; message.senderUsername = messageUser.username;
await db.insert( await db.insert(
@ -166,7 +168,7 @@ class MessagesService {
static Future<void> updateMessageThreads({List<Conversation>? conversations}) async { static Future<void> updateMessageThreads({List<Conversation>? conversations}) async {
MyProfile profile = await MyProfile.getProfile(); MyProfile profile = await MyProfile.getProfile();
conversations ??= await getConversations();
conversations ??= await ConversationsRepository.getConversations();
for (var i = 0; i < conversations.length; i++) { for (var i = 0; i < conversations.length; i++) {
await updateMessageThread(conversations[i], profile: profile); await updateMessageThread(conversations[i], profile: profile);


+ 3
- 2
mobile/lib/views/main/conversation/detail.dart View File

@ -1,5 +1,6 @@
import 'dart:io'; import 'dart:io';
import 'package:Envelope/database/repositories/messages_repository.dart';
import 'package:Envelope/services/messages_service.dart'; import 'package:Envelope/services/messages_service.dart';
import 'package:Envelope/views/main/conversation/message.dart'; import 'package:Envelope/views/main/conversation/message.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -78,7 +79,7 @@ class _ConversationDetailState extends State<ConversationDetail> {
Future<void> fetchMessages() async { Future<void> fetchMessages() async {
profile = await MyProfile.getProfile(); profile = await MyProfile.getProfile();
messages = await getMessagesForThread(widget.conversation);
messages = await MessagesRepository.getMessagesForThread(widget.conversation);
setState(() {}); setState(() {});
} }
@ -263,7 +264,7 @@ class _ConversationDetailState extends State<ConversationDetail> {
data: msgController.text != '' ? msgController.text : null, data: msgController.text != '' ? msgController.text : null,
files: selectedImages, files: selectedImages,
); );
messages = await getMessagesForThread(widget.conversation);
messages = await MessagesRepository.getMessagesForThread(widget.conversation);
setState(() { setState(() {
msgController.text = ''; msgController.text = '';
selectedImages = []; selectedImages = [];


+ 5
- 3
mobile/lib/views/main/conversation/list.dart View File

@ -2,6 +2,8 @@ import 'dart:io';
import 'package:Envelope/components/custom_title_bar.dart'; import 'package:Envelope/components/custom_title_bar.dart';
import 'package:Envelope/components/flash_message.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:Envelope/services/conversations_service.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -74,7 +76,7 @@ class _ConversationListState extends State<ConversationList> {
MaterialPageRoute(builder: (context) => ConversationAddFriendsList( MaterialPageRoute(builder: (context) => ConversationAddFriendsList(
friends: friends, friends: friends,
saveCallback: (List<Friend> friendsSelected) async { saveCallback: (List<Friend> friendsSelected) async {
Conversation conversation = await createConversation(
Conversation conversation = await ConversationsRepository.createConversation(
conversationName, conversationName,
friendsSelected, friendsSelected,
false, false,
@ -166,8 +168,8 @@ class _ConversationListState extends State<ConversationList> {
} }
onGoBack(dynamic value) async { onGoBack(dynamic value) async {
conversations = await getConversations();
friends = await getFriends();
conversations = await ConversationsRepository.getConversations();
friends = await FriendsRepository.getFriends();
setState(() {}); setState(() {});
} }
} }

+ 2
- 1
mobile/lib/views/main/conversation/list_item.dart View File

@ -1,3 +1,4 @@
import 'package:Envelope/database/repositories/conversations_repository.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '/database/models/messages.dart'; import '/database/models/messages.dart';
@ -113,7 +114,7 @@ class _ConversationListItemState extends State<ConversationListItem> {
} }
onGoBack(dynamic value) async { onGoBack(dynamic value) async {
conversation = await getConversationById(widget.conversation.id);
conversation = await ConversationsRepository.getConversationById(widget.conversation.id);
setState(() {}); setState(() {});
} }
} }

+ 2
- 1
mobile/lib/views/main/conversation/settings.dart View File

@ -1,6 +1,7 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:Envelope/database/repositories/conversation_users_repository.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
@ -156,7 +157,7 @@ class _ConversationSettingsState extends State<ConversationSettings> {
} }
Future<void> getUsers() async { Future<void> getUsers() async {
users = await getConversationUsers(widget.conversation);
users = await ConversationUsersRepository.getConversationUsers(widget.conversation);
profile = await MyProfile.getProfile(); profile = await MyProfile.getProfile();
setState(() {}); setState(() {});
} }


+ 3
- 2
mobile/lib/views/main/friend/list.dart View File

@ -1,3 +1,4 @@
import 'package:Envelope/database/repositories/friends_repository.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '/components/custom_title_bar.dart'; import '/components/custom_title_bar.dart';
@ -148,8 +149,8 @@ class _FriendListState extends State<FriendList> {
} }
Future<void> initFriends() async { Future<void> 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(() {}); setState(() {});
widget.callback(); widget.callback();
} }


+ 3
- 2
mobile/lib/views/main/friend/list_item.dart View File

@ -1,3 +1,4 @@
import 'package:Envelope/database/repositories/conversations_repository.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '/components/custom_circle_avatar.dart'; import '/components/custom_circle_avatar.dart';
@ -61,9 +62,9 @@ class _FriendListItemState extends State<FriendListItem> {
} }
Future<void> findOrCreateConversation(BuildContext context) async { Future<void> 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), generateRandomString(32),
[ widget.friend ], [ widget.friend ],
true, true,


+ 8
- 6
mobile/lib/views/main/home.dart View File

@ -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:flutter/material.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
@ -157,9 +159,9 @@ class _HomeState extends State<Home> {
await ConversationsService.updateConversations(); await ConversationsService.updateConversations();
await MessagesService.updateMessageThreads(); 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(); profile = await MyProfile.getProfile();
setState(() { setState(() {
@ -181,9 +183,9 @@ class _HomeState extends State<Home> {
Future<void> reinitDatabaseRecords() async { Future<void> 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(); profile = await MyProfile.getProfile();
setState(() { setState(() {


Loading…
Cancel
Save