Encrypted messaging app
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

228 lines
6.7 KiB

  1. import 'dart:convert';
  2. import 'dart:io';
  3. import 'dart:typed_data';
  4. import 'package:Envelope/database/models/conversation_users.dart';
  5. import 'package:sqflite/sql.dart';
  6. import 'package:uuid/uuid.dart';
  7. import '/database/models/conversations.dart';
  8. import '/database/models/friends.dart';
  9. import '/database/models/my_profile.dart';
  10. import '/utils/encryption/aes_helper.dart';
  11. import '/utils/strings.dart';
  12. import '/utils/storage/database.dart';
  13. class ConversationsRepository {
  14. static Future<Conversation> createConversation(String title, List<Friend> friends, bool twoUser) async {
  15. final db = await getDatabaseConnection();
  16. MyProfile profile = await MyProfile.getProfile();
  17. var uuid = const Uuid();
  18. final String conversationId = uuid.v4();
  19. Uint8List symmetricKey = AesHelper.deriveKey(generateRandomString(32));
  20. Conversation conversation = Conversation(
  21. id: conversationId,
  22. userId: profile.id,
  23. symmetricKey: base64.encode(symmetricKey),
  24. admin: true,
  25. name: title,
  26. twoUser: twoUser,
  27. status: ConversationStatus.pending,
  28. isRead: true,
  29. messageExpiryDefault: 'no_expiry',
  30. adminAddMembers: true,
  31. adminEditInfo: true,
  32. adminSendMessages: false,
  33. createdAt: DateTime.now(),
  34. updatedAt: DateTime.now(),
  35. );
  36. await db.insert(
  37. 'conversations',
  38. conversation.toMap(),
  39. conflictAlgorithm: ConflictAlgorithm.replace,
  40. );
  41. await db.insert(
  42. 'conversation_users',
  43. ConversationUser(
  44. id: uuid.v4(),
  45. userId: profile.id,
  46. conversationId: conversationId,
  47. username: profile.username,
  48. associationKey: uuid.v4(),
  49. publicKey: profile.publicKey!,
  50. admin: true,
  51. ).toMap(),
  52. conflictAlgorithm: ConflictAlgorithm.fail,
  53. );
  54. for (Friend friend in friends) {
  55. await db.insert(
  56. 'conversation_users',
  57. ConversationUser(
  58. id: uuid.v4(),
  59. userId: friend.friendId,
  60. conversationId: conversationId,
  61. username: friend.username,
  62. associationKey: uuid.v4(),
  63. publicKey: friend.publicKey,
  64. admin: twoUser ? true : false,
  65. ).toMap(),
  66. conflictAlgorithm: ConflictAlgorithm.replace,
  67. );
  68. }
  69. if (twoUser) {
  70. List<Map<String, dynamic>> maps = await db.query(
  71. 'conversation_users',
  72. where: 'conversation_id = ? AND user_id != ?',
  73. whereArgs: [ conversation.id, profile.id ],
  74. );
  75. if (maps.length != 1) {
  76. throw ArgumentError('Invalid user id');
  77. }
  78. conversation.name = maps[0]['username'];
  79. await db.insert(
  80. 'conversations',
  81. conversation.toMap(),
  82. conflictAlgorithm: ConflictAlgorithm.replace,
  83. );
  84. }
  85. return conversation;
  86. }
  87. static Future<Conversation> getConversationById(String id) async {
  88. final db = await getDatabaseConnection();
  89. final List<Map<String, dynamic>> maps = await db.query(
  90. 'conversations',
  91. where: 'id = ?',
  92. whereArgs: [id],
  93. );
  94. if (maps.length != 1) {
  95. throw ArgumentError('Invalid user id');
  96. }
  97. File? file;
  98. if (maps[0]['file'] != null && maps[0]['file'] != '') {
  99. file = File(maps[0]['file']);
  100. }
  101. return Conversation(
  102. id: maps[0]['id'],
  103. userId: maps[0]['user_id'],
  104. symmetricKey: maps[0]['symmetric_key'],
  105. admin: maps[0]['admin'] == 1,
  106. name: maps[0]['name'],
  107. twoUser: maps[0]['two_user'] == 1,
  108. status: ConversationStatus.values[maps[0]['status']],
  109. isRead: maps[0]['is_read'] == 1,
  110. icon: file,
  111. messageExpiryDefault: maps[0]['message_expiry'],
  112. adminAddMembers: maps[0]['admin_add_members'] == 1,
  113. adminEditInfo: maps[0]['admin_edit_info'] == 1,
  114. adminSendMessages: maps[0]['admin_send_messages'] == 1,
  115. createdAt: DateTime.parse(maps[0]['created_at']),
  116. updatedAt: DateTime.parse(maps[0]['updated_at']),
  117. );
  118. }
  119. static Future<List<Conversation>> getConversations() async {
  120. final db = await getDatabaseConnection();
  121. final List<Map<String, dynamic>> maps = await db.query(
  122. 'conversations',
  123. orderBy: 'name',
  124. );
  125. return List.generate(maps.length, (i) {
  126. File? file;
  127. if (maps[i]['file'] != null && maps[i]['file'] != '') {
  128. file = File(maps[i]['file']);
  129. }
  130. return Conversation(
  131. id: maps[i]['id'],
  132. userId: maps[i]['user_id'],
  133. symmetricKey: maps[i]['symmetric_key'],
  134. admin: maps[i]['admin'] == 1,
  135. name: maps[i]['name'],
  136. twoUser: maps[i]['two_user'] == 1,
  137. status: ConversationStatus.values[maps[i]['status']],
  138. isRead: maps[i]['is_read'] == 1,
  139. icon: file,
  140. messageExpiryDefault: maps[i]['message_expiry'] ?? 'no_expiry',
  141. adminAddMembers: maps[i]['admin_add_members'] == 1,
  142. adminEditInfo: maps[i]['admin_edit_info'] == 1,
  143. adminSendMessages: maps[i]['admin_send_messages'] == 1,
  144. createdAt: DateTime.parse(maps[i]['created_at']),
  145. updatedAt: DateTime.parse(maps[i]['updated_at']),
  146. );
  147. });
  148. }
  149. static Future<Conversation?> getTwoUserConversation(String userId) async {
  150. final db = await getDatabaseConnection();
  151. MyProfile profile = await MyProfile.getProfile();
  152. final List<Map<String, dynamic>> maps = await db.rawQuery(
  153. '''
  154. SELECT conversations.* FROM conversations
  155. LEFT JOIN conversation_users ON conversation_users.conversation_id = conversations.id
  156. WHERE conversation_users.user_id = ?
  157. AND conversation_users.user_id != ?
  158. AND conversations.two_user = 1
  159. ''',
  160. [ userId, profile.id ],
  161. );
  162. if (maps.length != 1) {
  163. return null;
  164. }
  165. return Conversation(
  166. id: maps[0]['id'],
  167. userId: maps[0]['user_id'],
  168. symmetricKey: maps[0]['symmetric_key'],
  169. admin: maps[0]['admin'] == 1,
  170. name: maps[0]['name'],
  171. twoUser: maps[0]['two_user'] == 1,
  172. status: ConversationStatus.values[maps[0]['status']],
  173. isRead: maps[0]['is_read'] == 1,
  174. messageExpiryDefault: maps[0]['message_expiry'],
  175. adminAddMembers: maps[0]['admin_add_members'] == 1,
  176. adminEditInfo: maps[0]['admin_edit_info'] == 1,
  177. adminSendMessages: maps[0]['admin_send_messages'] == 1,
  178. createdAt: DateTime.parse(maps[0]['created_at']),
  179. updatedAt: DateTime.parse(maps[0]['updated_at']),
  180. );
  181. }
  182. static Future<DateTime?> getLatestUpdatedAt() async {
  183. final db = await getDatabaseConnection();
  184. final List<Map<String, dynamic>> maps = await db.rawQuery(
  185. '''
  186. SELECT conversations.updated_at FROM conversations
  187. ORDER BY updated_at DESC
  188. LIMIT 1;
  189. '''
  190. );
  191. return maps.isNotEmpty ? DateTime.parse(maps[0]['updated_at']) : null;
  192. }
  193. }