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