From d7af0a9ac940e8efc06774541fb5d9155e2e9e81 Mon Sep 17 00:00:00 2001 From: Tovi Jaeschke-Rogers Date: Tue, 2 Aug 2022 16:11:20 +0930 Subject: [PATCH] Working conversations and messages --- Backend/Api/Messages/CreateConversation.go | 7 +- Backend/Api/Messages/UpdateConversation.go | 56 +++++ Backend/Api/Routes.go | 1 + Backend/Database/Seeder/MessageSeeder.go | 33 ++- Backend/Database/UserConversations.go | 35 +++ Backend/Models/Messages.go | 19 +- Backend/main.go | 12 +- README.md | 16 +- mobile/lib/models/conversation_users.dart | 154 ++++++------ mobile/lib/models/conversations.dart | 41 +++- mobile/lib/models/messages.dart | 232 +++++++++--------- mobile/lib/utils/storage/conversations.dart | 27 +- mobile/lib/utils/storage/messages.dart | 3 +- .../main/conversation/create_add_users.dart | 16 +- .../views/main/conversation/edit_details.dart | 22 +- mobile/lib/views/main/conversation/list.dart | 26 +- .../lib/views/main/conversation/settings.dart | 107 ++++++-- 17 files changed, 537 insertions(+), 270 deletions(-) create mode 100644 Backend/Api/Messages/UpdateConversation.go diff --git a/Backend/Api/Messages/CreateConversation.go b/Backend/Api/Messages/CreateConversation.go index a806473..505305a 100644 --- a/Backend/Api/Messages/CreateConversation.go +++ b/Backend/Api/Messages/CreateConversation.go @@ -2,7 +2,6 @@ package Messages import ( "encoding/json" - "fmt" "net/http" "github.com/gofrs/uuid" @@ -11,7 +10,7 @@ import ( "git.tovijaeschke.xyz/tovi/Envelope/Backend/Models" ) -type RawConversationData struct { +type RawCreateConversationData struct { ID string `json:"id"` Name string `json:"name"` Users string `json:"users"` @@ -20,7 +19,7 @@ type RawConversationData struct { func CreateConversation(w http.ResponseWriter, r *http.Request) { var ( - rawConversationData RawConversationData + rawConversationData RawCreateConversationData messageThread Models.ConversationDetail err error ) @@ -45,8 +44,6 @@ func CreateConversation(w http.ResponseWriter, r *http.Request) { return } - fmt.Println(rawConversationData.UserConversations[0]) - err = Database.CreateUserConversations(&rawConversationData.UserConversations) if err != nil { http.Error(w, "Error", http.StatusInternalServerError) diff --git a/Backend/Api/Messages/UpdateConversation.go b/Backend/Api/Messages/UpdateConversation.go new file mode 100644 index 0000000..f1073a2 --- /dev/null +++ b/Backend/Api/Messages/UpdateConversation.go @@ -0,0 +1,56 @@ +package Messages + +import ( + "encoding/json" + "net/http" + + "github.com/gofrs/uuid" + + "git.tovijaeschke.xyz/tovi/Envelope/Backend/Database" + "git.tovijaeschke.xyz/tovi/Envelope/Backend/Models" +) + +type RawUpdateConversationData struct { + ID string `json:"id"` + Name string `json:"name"` + Users string `json:"users"` + UserConversations []Models.UserConversation `json:"user_conversations"` +} + +func UpdateConversation(w http.ResponseWriter, r *http.Request) { + var ( + rawConversationData RawCreateConversationData + messageThread Models.ConversationDetail + err error + ) + + err = json.NewDecoder(r.Body).Decode(&rawConversationData) + if err != nil { + http.Error(w, "Error", http.StatusInternalServerError) + return + } + + messageThread = Models.ConversationDetail{ + Base: Models.Base{ + ID: uuid.FromStringOrNil(rawConversationData.ID), + }, + Name: rawConversationData.Name, + Users: rawConversationData.Users, + } + + err = Database.UpdateConversationDetail(&messageThread) + if err != nil { + http.Error(w, "Error", http.StatusInternalServerError) + return + } + + if len(rawConversationData.UserConversations) > 0 { + err = Database.UpdateOrCreateUserConversations(&rawConversationData.UserConversations) + if err != nil { + http.Error(w, "Error", http.StatusInternalServerError) + return + } + } + + w.WriteHeader(http.StatusOK) +} diff --git a/Backend/Api/Routes.go b/Backend/Api/Routes.go index a37fc15..e1f8df4 100644 --- a/Backend/Api/Routes.go +++ b/Backend/Api/Routes.go @@ -67,6 +67,7 @@ func InitApiEndpoints(router *mux.Router) { authApi.HandleFunc("/conversation_details", Messages.EncryptedConversationDetailsList).Methods("GET") authApi.HandleFunc("/conversations", Messages.CreateConversation).Methods("POST") + authApi.HandleFunc("/conversations", Messages.UpdateConversation).Methods("PUT") // Define routes for messages authApi.HandleFunc("/message", Messages.CreateMessage).Methods("POST") diff --git a/Backend/Database/Seeder/MessageSeeder.go b/Backend/Database/Seeder/MessageSeeder.go index 76ec995..0aa750f 100644 --- a/Backend/Database/Seeder/MessageSeeder.go +++ b/Backend/Database/Seeder/MessageSeeder.go @@ -6,7 +6,6 @@ import ( "git.tovijaeschke.xyz/tovi/Envelope/Backend/Database" "git.tovijaeschke.xyz/tovi/Envelope/Backend/Models" - "git.tovijaeschke.xyz/tovi/Envelope/Backend/Util" "github.com/gofrs/uuid" ) @@ -176,9 +175,9 @@ func SeedMessages() { messageThread Models.ConversationDetail key aesKey primaryUser Models.User - primaryUserAssociationKey string + primaryUserAssociationKey uuid.UUID secondaryUser Models.User - secondaryUserAssociationKey string + secondaryUserAssociationKey uuid.UUID userJson string id1, id2 uuid.UUID i int @@ -186,10 +185,19 @@ func SeedMessages() { ) key, err = generateAesKey() + if err != nil { + panic(err) + } messageThread, err = seedConversationDetail(key) - primaryUserAssociationKey = Util.RandomString(32) - secondaryUserAssociationKey = Util.RandomString(32) + primaryUserAssociationKey, err = uuid.NewV4() + if err != nil { + panic(err) + } + secondaryUserAssociationKey, err = uuid.NewV4() + if err != nil { + panic(err) + } primaryUser, err = Database.GetUserByUsername("testUser") if err != nil { @@ -201,6 +209,9 @@ func SeedMessages() { messageThread.ID, key, ) + if err != nil { + panic(err) + } secondaryUser, err = Database.GetUserByUsername("ATestUser2") if err != nil { @@ -227,14 +238,14 @@ func SeedMessages() { [ { "id": "%s", - "user_id": "%s", + "user_id": "%s", "username": "%s", "admin": "true", "association_key": "%s" }, { "id": "%s", - "user_id": "%s", + "user_id": "%s", "username": "%s", "admin": "false", "association_key": "%s" @@ -244,11 +255,11 @@ func SeedMessages() { id1.String(), primaryUser.ID.String(), primaryUser.Username, - primaryUserAssociationKey, + primaryUserAssociationKey.String(), id2.String(), secondaryUser.ID.String(), secondaryUser.Username, - secondaryUserAssociationKey, + secondaryUserAssociationKey.String(), ) messageThread, err = seedUpdateUserConversation( @@ -261,8 +272,8 @@ func SeedMessages() { err = seedMessage( primaryUser, secondaryUser, - primaryUserAssociationKey, - secondaryUserAssociationKey, + primaryUserAssociationKey.String(), + secondaryUserAssociationKey.String(), i, ) if err != nil { diff --git a/Backend/Database/UserConversations.go b/Backend/Database/UserConversations.go index 02fb62d..930a98f 100644 --- a/Backend/Database/UserConversations.go +++ b/Backend/Database/UserConversations.go @@ -4,6 +4,7 @@ import ( "git.tovijaeschke.xyz/tovi/Envelope/Backend/Models" "gorm.io/gorm" + "gorm.io/gorm/clause" ) func GetUserConversationById(id string) (Models.UserConversation, error) { @@ -49,6 +50,40 @@ func CreateUserConversations(userConversations *[]Models.UserConversation) error return err } +func UpdateUserConversation(userConversation *Models.UserConversation) error { + var err error + + err = DB.Model(Models.UserConversation{}). + Updates(userConversation). + Error + + return err +} + +func UpdateUserConversations(userConversations *[]Models.UserConversation) error { + var err error + + err = DB.Model(Models.UserConversation{}). + Updates(userConversations). + Error + + return err +} + +func UpdateOrCreateUserConversations(userConversations *[]Models.UserConversation) error { + var err error + + err = DB.Model(Models.UserConversation{}). + Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "id"}}, + DoUpdates: clause.AssignmentColumns([]string{"admin"}), + }). + Create(userConversations). + Error + + return err +} + func DeleteUserConversation(userConversation *Models.UserConversation) error { return DB.Session(&gorm.Session{FullSaveAssociations: true}). Delete(userConversation). diff --git a/Backend/Models/Messages.go b/Backend/Models/Messages.go index fed8909..de32fca 100644 --- a/Backend/Models/Messages.go +++ b/Backend/Models/Messages.go @@ -18,22 +18,23 @@ type Message struct { Base MessageDataID uuid.UUID `json:"message_data_id"` MessageData MessageData `json:"message_data"` - SymmetricKey string `gorm:"not null" json:"symmetric_key"` // Stored encrypted - AssociationKey string `gorm:"not null" json:"association_key"` // TODO: This links all encrypted messages for a user in a thread together. Find a way to fix this - CreatedAt time.Time `gorm:"not null" json:"created_at"` + SymmetricKey string `json:"symmetric_key" gorm:"not null"` // Stored encrypted + AssociationKey string `json:"association_key" gorm:"not null"` // TODO: This links all encrypted messages for a user in a thread together. Find a way to fix this + CreatedAt time.Time `json:"created_at" gorm:"not null"` } type ConversationDetail struct { Base - Name string `gorm:"not null" json:"name"` // Stored encrypted - Users string `json:"users"` // Stored as encrypted JSON + Name string `gorm:"not null" json:"name"` // Stored encrypted + Users string ` json:"users"` // Stored as encrypted JSON } type UserConversation struct { Base UserID uuid.UUID `gorm:"type:uuid;column:user_id;not null;" json:"user_id"` - User User `json:"user"` - ConversationDetailID string `gorm:"not null" json:"conversation_detail_id"` // Stored encrypted - Admin string `gorm:"not null" json:"admin"` // Bool if user is admin of thread, stored encrypted - SymmetricKey string `gorm:"not null" json:"symmetric_key"` // Stored encrypted + User User ` json:"user"` + ConversationDetailID string `gorm:"not null" json:"conversation_detail_id"` // Stored encrypted + Admin string `gorm:"not null" json:"admin"` // Bool if user is admin of thread, stored encrypted + SymmetricKey string `gorm:"not null" json:"symmetric_key"` // Stored encrypted + // TODO: Add association_key here } diff --git a/Backend/main.go b/Backend/main.go index 2978a6f..00db155 100644 --- a/Backend/main.go +++ b/Backend/main.go @@ -2,6 +2,7 @@ package main import ( "flag" + "fmt" "net/http" "git.tovijaeschke.xyz/tovi/Envelope/Backend/Api" @@ -11,9 +12,7 @@ import ( "github.com/gorilla/mux" ) -var ( - seed bool -) +var seed bool func init() { Database.Init() @@ -26,6 +25,7 @@ func init() { func main() { var ( router *mux.Router + err error ) if seed { @@ -37,5 +37,9 @@ func main() { Api.InitApiEndpoints(router) - http.ListenAndServe(":8080", router) + fmt.Println("Listening on port :8080") + err = http.ListenAndServe(":8080", router) + if err != nil { + panic(err) + } } diff --git a/README.md b/README.md index dd411b9..d1cb191 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,17 @@ # Envelope -Encrypted messaging app \ No newline at end of file +Encrypted messaging app + +## TODO + +- Fix adding users to conversations +- Fix users recieving messages +- Fix the admin checks on conversation settings page +- Add admin checks to conversation settings page +- Add admin checks on backend +- Add errors to login / signup page +- Add errors when updating conversations +- Refactor the update conversations function +- Finish the friends list page +- Allow adding friends +- Finish the disappearing messages functionality diff --git a/mobile/lib/models/conversation_users.dart b/mobile/lib/models/conversation_users.dart index 3417792..fe68f58 100644 --- a/mobile/lib/models/conversation_users.dart +++ b/mobile/lib/models/conversation_users.dart @@ -2,97 +2,97 @@ import '/models/conversations.dart'; import '/utils/storage/database.dart'; Future getConversationUser(Conversation conversation, String userId) async { - final db = await getDatabaseConnection(); + final db = await getDatabaseConnection(); - final List> maps = await db.query( - 'conversation_users', - where: 'conversation_id = ? AND user_id = ?', - whereArgs: [conversation.id, userId], - ); + 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'); - } + 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'], - admin: maps[0]['admin'] == 1, - ); + 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'], + 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 db = await getDatabaseConnection(); - final List> maps = await db.query( - 'conversation_users', - where: 'conversation_id = ?', - whereArgs: [conversation.id], - orderBy: 'admin', - ); + final List> maps = await db.query( + 'conversation_users', + where: 'conversation_id = ?', + whereArgs: [conversation.id], + orderBy: 'admin', + ); - return 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'], - admin: maps[i]['admin'] == 1, - ); - }); + return 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'], + admin: maps[i]['admin'] == 1, + ); + }); } class ConversationUser{ - String id; - String userId; - String conversationId; - String username; - String associationKey; - bool admin; - ConversationUser({ - required this.id, - required this.userId, - required this.conversationId, - required this.username, - required this.associationKey, - required this.admin, - }); + String id; + String userId; + String conversationId; + String username; + String associationKey; + bool admin; + ConversationUser({ + required this.id, + required this.userId, + required this.conversationId, + required this.username, + required this.associationKey, + required this.admin, + }); - factory ConversationUser.fromJson(Map json, String conversationId) { - return ConversationUser( - id: json['id'], - userId: json['user_id'], - conversationId: conversationId, - username: json['username'], - associationKey: json['association_key'], - admin: json['admin'] == 'true', - ); - } + factory ConversationUser.fromJson(Map json, String conversationId) { + return ConversationUser( + id: json['id'], + userId: json['user_id'], + conversationId: conversationId, + username: json['username'], + associationKey: json['association_key'], + admin: json['admin'] == 'true', + ); + } - Map toJson() { - return { - 'id': id, - 'user_id': userId, - 'username': username, - 'association_key': associationKey, - 'admin': admin ? 'true' : 'false', - }; - } + Map toJson() { + return { + 'id': id, + 'user_id': userId, + 'username': username, + 'association_key': associationKey, + 'admin': admin ? 'true' : 'false', + }; + } - Map toMap() { - return { - 'id': id, - 'user_id': userId, - 'conversation_id': conversationId, - 'username': username, - 'association_key': associationKey, - 'admin': admin ? 1 : 0, - }; - } + Map toMap() { + return { + 'id': id, + 'user_id': userId, + 'conversation_id': conversationId, + 'username': username, + 'association_key': associationKey, + 'admin': admin ? 1 : 0, + }; + } } diff --git a/mobile/lib/models/conversations.dart b/mobile/lib/models/conversations.dart index 88adea3..6e656da 100644 --- a/mobile/lib/models/conversations.dart +++ b/mobile/lib/models/conversations.dart @@ -25,8 +25,6 @@ Future createConversation(String title, List friends) asyn Uint8List symmetricKey = AesHelper.deriveKey(generateRandomString(32)); - String associationKey = generateRandomString(32); - Conversation conversation = Conversation( id: conversationId, userId: profile.id, @@ -51,7 +49,7 @@ Future createConversation(String title, List friends) asyn userId: profile.id, conversationId: conversationId, username: profile.username, - associationKey: associationKey, + associationKey: uuid.v4(), admin: true, ).toMap(), conflictAlgorithm: ConflictAlgorithm.fail, @@ -65,7 +63,31 @@ Future createConversation(String title, List friends) asyn userId: friend.friendId, conversationId: conversationId, username: friend.username, - associationKey: associationKey, + associationKey: uuid.v4(), + admin: false, + ).toMap(), + conflictAlgorithm: ConflictAlgorithm.replace, + ); + } + + return conversation; +} + + +Future addUsersToConversation(Conversation conversation, List friends) async { + final db = await getDatabaseConnection(); + + var uuid = const Uuid(); + + for (Friend friend in friends) { + await db.insert( + 'conversation_users', + ConversationUser( + id: uuid.v4(), + userId: friend.friendId, + conversationId: conversation.id, + username: friend.username, + associationKey: uuid.v4(), admin: false, ).toMap(), conflictAlgorithm: ConflictAlgorithm.replace, @@ -181,12 +203,21 @@ class Conversation { ); } - Future> toJson() async { + Future> payloadJson({ bool includeUsers = true }) async { MyProfile profile = await MyProfile.getProfile(); var symKey = base64.decode(symmetricKey); List users = await getConversationUsers(this); + + if (!includeUsers) { + return { + 'id': conversationDetailId, + 'name': AesHelper.aesEncrypt(symKey, Uint8List.fromList(name.codeUnits)), + 'users': AesHelper.aesEncrypt(symKey, Uint8List.fromList(jsonEncode(users).codeUnits)), + }; + } + List userConversations = []; for (ConversationUser user in users) { diff --git a/mobile/lib/models/messages.dart b/mobile/lib/models/messages.dart index ae0edac..f19f78c 100644 --- a/mobile/lib/models/messages.dart +++ b/mobile/lib/models/messages.dart @@ -1,17 +1,48 @@ import 'dart:convert'; import 'dart:typed_data'; -import 'package:Envelope/models/conversation_users.dart'; -import 'package:Envelope/models/conversations.dart'; + import 'package:pointycastle/export.dart'; -import 'package:shared_preferences/shared_preferences.dart'; -import '/utils/encryption/crypto_utils.dart'; + +import '/models/conversation_users.dart'; +import '/models/conversations.dart'; +import '/models/my_profile.dart'; +import '/models/friends.dart'; import '/utils/encryption/aes_helper.dart'; +import '/utils/encryption/crypto_utils.dart'; import '/utils/storage/database.dart'; import '/utils/strings.dart'; -import '/models/friends.dart'; -const messageTypeSender = 'sender'; 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) { + return Message( + id: maps[i]['id'], + symmetricKey: maps[i]['symmetric_key'], + userSymmetricKey: maps[i]['user_symmetric_key'], + data: 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; @@ -38,109 +69,99 @@ class Message { factory Message.fromJson(Map json, RSAPrivateKey privKey) { var userSymmetricKey = CryptoUtils.rsaDecrypt( - base64.decode(json['symmetric_key']), - privKey, + base64.decode(json['symmetric_key']), + privKey, ); var symmetricKey = AesHelper.aesDecrypt( - userSymmetricKey, - base64.decode(json['message_data']['symmetric_key']), + userSymmetricKey, + base64.decode(json['message_data']['symmetric_key']), ); var senderId = AesHelper.aesDecrypt( - base64.decode(symmetricKey), - base64.decode(json['message_data']['sender_id']), + base64.decode(symmetricKey), + base64.decode(json['message_data']['sender_id']), ); var data = AesHelper.aesDecrypt( - base64.decode(symmetricKey), - base64.decode(json['message_data']['data']), + base64.decode(symmetricKey), + base64.decode(json['message_data']['data']), ); return Message( - id: json['id'], - symmetricKey: symmetricKey, - userSymmetricKey: base64.encode(userSymmetricKey), - data: data, - senderId: senderId, - senderUsername: 'Unknown', - associationKey: json['association_key'], - createdAt: json['created_at'], - failedToSend: false, + id: json['id'], + symmetricKey: symmetricKey, + userSymmetricKey: base64.encode(userSymmetricKey), + data: data, + senderId: senderId, + senderUsername: 'Unknown', + associationKey: json['association_key'], + createdAt: json['created_at'], + failedToSend: false, ); } Future toJson(Conversation conversation, String messageDataId) async { - final preferences = await SharedPreferences.getInstance(); - RSAPublicKey publicKey = CryptoUtils.rsaPublicKeyFromPem(preferences.getString('asymmetricPublicKey')!); - - final userSymmetricKey = AesHelper.deriveKey(generateRandomString(32)); - final symmetricKey = AesHelper.deriveKey(generateRandomString(32)); - - List> messages = []; - - String id = ''; - - List conversationUsers = await getConversationUsers(conversation); - - for (var i = 0; i < conversationUsers.length; i++) { - ConversationUser user = conversationUsers[i]; - if (preferences.getString('username') == user.username) { - id = user.id; - - messages.add({ - 'message_data_id': messageDataId, - 'symmetric_key': base64.encode(CryptoUtils.rsaEncrypt( - userSymmetricKey, - publicKey, - )), - 'association_key': user.associationKey, - }); - - continue; - } - - Friend friend = await getFriendByFriendId(user.id); - RSAPublicKey friendPublicKey = CryptoUtils.rsaPublicKeyFromPem(friend.asymmetricPublicKey); - - messages.add({ - 'message_data_id': messageDataId, - 'symmetric_key': base64.encode(CryptoUtils.rsaEncrypt( - userSymmetricKey, - friendPublicKey, - )), - 'association_key': user.associationKey, - }); - } + MyProfile profile = await MyProfile.getProfile(); + if (profile.publicKey == null) { + throw Exception('Could not get profile.publicKey'); + } + RSAPublicKey publicKey = profile.publicKey!; + + final userSymmetricKey = AesHelper.deriveKey(generateRandomString(32)); + final symmetricKey = AesHelper.deriveKey(generateRandomString(32)); + + List> messages = []; + + String id = ''; + + List conversationUsers = await getConversationUsers(conversation); - Map messageData = { - 'id': messageDataId, - 'data': AesHelper.aesEncrypt(symmetricKey, Uint8List.fromList(data.codeUnits)), - 'sender_id': AesHelper.aesEncrypt(symmetricKey, Uint8List.fromList(id.codeUnits)), - 'symmetric_key': AesHelper.aesEncrypt( + for (var i = 0; i < conversationUsers.length; i++) { + ConversationUser user = conversationUsers[i]; + + if (profile.id == user.userId) { + id = user.id; + + messages.add({ + 'message_data_id': messageDataId, + 'symmetric_key': base64.encode(CryptoUtils.rsaEncrypt( userSymmetricKey, - Uint8List.fromList(base64.encode(symmetricKey).codeUnits), - ), - }; + publicKey, + )), + 'association_key': user.associationKey, + }); - return jsonEncode({ - 'message_data': messageData, - 'message': messages, - }); - } + continue; + } - @override - String toString() { - return ''' + Friend friend = await getFriendByFriendId(user.userId); + RSAPublicKey friendPublicKey = CryptoUtils.rsaPublicKeyFromPem(friend.asymmetricPublicKey); + + messages.add({ + 'message_data_id': messageDataId, + 'symmetric_key': base64.encode(CryptoUtils.rsaEncrypt( + userSymmetricKey, + friendPublicKey, + )), + 'association_key': user.associationKey, + }); + } + Map messageData = { + 'id': messageDataId, + 'data': AesHelper.aesEncrypt(symmetricKey, Uint8List.fromList(data.codeUnits)), + 'sender_id': AesHelper.aesEncrypt(symmetricKey, Uint8List.fromList(senderId.codeUnits)), + 'symmetric_key': AesHelper.aesEncrypt( + userSymmetricKey, + Uint8List.fromList(base64.encode(symmetricKey).codeUnits), + ), + }; - id: $id - data: $data - senderId: $senderId - senderUsername: $senderUsername - associationKey: $associationKey - createdAt: $createdAt -'''; + return jsonEncode({ + 'message_data': messageData, + 'message': messages, + }); } Map toMap() { @@ -157,33 +178,18 @@ class Message { }; } -} - -Future> getMessagesForThread(Conversation conversation) async { - final db = await getDatabaseConnection(); + @override + String toString() { + return ''' - 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) { - return Message( - id: maps[i]['id'], - symmetricKey: maps[i]['symmetric_key'], - userSymmetricKey: maps[i]['user_symmetric_key'], - data: 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, - ); - }); + id: $id + data: $data + senderId: $senderId + senderUsername: $senderUsername + associationKey: $associationKey + createdAt: $createdAt + '''; + } } diff --git a/mobile/lib/utils/storage/conversations.dart b/mobile/lib/utils/storage/conversations.dart index 69d5f5c..a7fe2c0 100644 --- a/mobile/lib/utils/storage/conversations.dart +++ b/mobile/lib/utils/storage/conversations.dart @@ -31,6 +31,10 @@ Future updateConversations() async { List conversationsJson = jsonDecode(resp.body); + if (conversationsJson.isEmpty) { + return; + } + for (var i = 0; i < conversationsJson.length; i++) { Conversation conversation = Conversation.fromJson( conversationsJson[i] as Map, @@ -102,7 +106,7 @@ Future updateConversations() async { Future uploadConversation(Conversation conversation) async { String sessionCookie = await getSessionCookie(); - Map conversationJson = await conversation.toJson(); + Map conversationJson = await conversation.payloadJson(); var x = await http.post( Uri.parse('${dotenv.env["SERVER_URL"]}api/v1/auth/conversations'), @@ -113,5 +117,26 @@ Future uploadConversation(Conversation conversation) async { body: jsonEncode(conversationJson), ); + // TODO: Handle errors here + print(x.statusCode); +} + + +Future updateConversation(Conversation conversation, { includeUsers = true } ) async { + String sessionCookie = await getSessionCookie(); + + Map conversationJson = await conversation.payloadJson(includeUsers: includeUsers); + + var x = await http.put( + Uri.parse('${dotenv.env["SERVER_URL"]}api/v1/auth/conversations'), + headers: { + 'Content-Type': 'application/json; charset=UTF-8', + 'cookie': sessionCookie, + }, + body: jsonEncode(conversationJson), + ); + + // TODO: Handle errors here print(x.statusCode); } + diff --git a/mobile/lib/utils/storage/messages.dart b/mobile/lib/utils/storage/messages.dart index 128c84d..d5978d8 100644 --- a/mobile/lib/utils/storage/messages.dart +++ b/mobile/lib/utils/storage/messages.dart @@ -24,7 +24,7 @@ Future sendMessage(Conversation conversation, String data) async { id: messageDataId, symmetricKey: '', userSymmetricKey: '', - senderId: profile.id, + senderId: currentUser.userId, senderUsername: profile.username, data: data, associationKey: currentUser.associationKey, @@ -66,6 +66,7 @@ Future sendMessage(Conversation conversation, String data) async { where: 'id = ?', whereArgs: [message.id], ); + throw exception; }); } diff --git a/mobile/lib/views/main/conversation/create_add_users.dart b/mobile/lib/views/main/conversation/create_add_users.dart index 4e9ed49..1b9186f 100644 --- a/mobile/lib/views/main/conversation/create_add_users.dart +++ b/mobile/lib/views/main/conversation/create_add_users.dart @@ -8,11 +8,11 @@ import '/views/main/conversation/detail.dart'; class ConversationAddFriendsList extends StatefulWidget { final List friends; - final String title; + final Function(List friendsSelected) saveCallback; const ConversationAddFriendsList({ Key? key, required this.friends, - required this.title, + required this.saveCallback, }) : super(key: key); @override @@ -88,20 +88,12 @@ class _ConversationAddFriendsListState extends State floatingActionButton: Padding( padding: const EdgeInsets.only(right: 10, bottom: 10), child: FloatingActionButton( - onPressed: () async { - Conversation conversation = await createConversation(widget.title, friendsSelected); - uploadConversation(conversation); + onPressed: () { + widget.saveCallback(friendsSelected); setState(() { friendsSelected = []; }); - - Navigator.of(context).popUntil((route) => route.isFirst); - Navigator.push(context, MaterialPageRoute(builder: (context){ - return ConversationDetail( - conversation: conversation, - ); - })); }, backgroundColor: Theme.of(context).colorScheme.primary, child: friendsSelected.isEmpty ? diff --git a/mobile/lib/views/main/conversation/edit_details.dart b/mobile/lib/views/main/conversation/edit_details.dart index 5309579..a0441b9 100644 --- a/mobile/lib/views/main/conversation/edit_details.dart +++ b/mobile/lib/views/main/conversation/edit_details.dart @@ -1,20 +1,15 @@ -import 'package:Envelope/models/conversation_users.dart'; import 'package:flutter/material.dart'; import '/components/custom_circle_avatar.dart'; import '/models/conversations.dart'; -import '/models/friends.dart'; -import '/views/main/conversation/create_add_users.dart'; class ConversationEditDetails extends StatefulWidget { + final Function(String conversationName) saveCallback; final Conversation? conversation; - final List? friends; - final List? users; const ConversationEditDetails({ Key? key, + required this.saveCallback, this.conversation, - this.friends, - this.users, }) : super(key: key); @override @@ -134,15 +129,12 @@ class _ConversationEditDetails extends State { ElevatedButton( style: buttonStyle, onPressed: () { - if (_formKey.currentState!.validate()) { - Navigator.of(context).push( - MaterialPageRoute(builder: (context) => ConversationAddFriendsList( - friends: widget.friends!, - title: conversationNameController.text, - ) - ) - ); + if (!_formKey.currentState!.validate()) { + // TODO: Show error here + return; } + + widget.saveCallback(conversationNameController.text); }, child: const Text('Save'), ), diff --git a/mobile/lib/views/main/conversation/list.dart b/mobile/lib/views/main/conversation/list.dart index 8ec0931..a41e6cf 100644 --- a/mobile/lib/views/main/conversation/list.dart +++ b/mobile/lib/views/main/conversation/list.dart @@ -1,9 +1,12 @@ import 'package:Envelope/models/friends.dart'; +import 'package:Envelope/utils/storage/conversations.dart'; import 'package:flutter/material.dart'; import '/models/conversations.dart'; import '/views/main/conversation/edit_details.dart'; import '/views/main/conversation/list_item.dart'; +import 'create_add_users.dart'; +import 'detail.dart'; class ConversationList extends StatefulWidget { final List conversations; @@ -73,7 +76,28 @@ class _ConversationListState extends State { onPressed: () { Navigator.of(context).push( MaterialPageRoute(builder: (context) => ConversationEditDetails( - friends: friends, + saveCallback: (String conversationName) { + Navigator.of(context).push( + MaterialPageRoute(builder: (context) => ConversationAddFriendsList( + friends: friends, + saveCallback: (List friendsSelected) async { + Conversation conversation = await createConversation( + conversationName, + friendsSelected + ); + + uploadConversation(conversation); + + Navigator.of(context).popUntil((route) => route.isFirst); + Navigator.push(context, MaterialPageRoute(builder: (context){ + return ConversationDetail( + conversation: conversation, + ); + })); + }, + )) + ); + }, )), ).then(onGoBack); }, diff --git a/mobile/lib/views/main/conversation/settings.dart b/mobile/lib/views/main/conversation/settings.dart index 2e8673c..cc6991b 100644 --- a/mobile/lib/views/main/conversation/settings.dart +++ b/mobile/lib/views/main/conversation/settings.dart @@ -1,11 +1,15 @@ -import 'package:Envelope/components/custom_circle_avatar.dart'; -import 'package:Envelope/views/main/conversation/edit_details.dart'; +import 'package:Envelope/models/friends.dart'; +import 'package:Envelope/views/main/conversation/create_add_users.dart'; import 'package:flutter/material.dart'; import '/models/conversation_users.dart'; import '/models/conversations.dart'; import '/models/my_profile.dart'; import '/views/main/conversation/settings_user_list_item.dart'; +import '/views/main/conversation/edit_details.dart'; +import '/components/custom_circle_avatar.dart'; +import '/utils/storage/database.dart'; +import '/utils/storage/conversations.dart'; class ConversationSettings extends StatefulWidget { final Conversation conversation; @@ -78,7 +82,7 @@ class _ConversationSettingsState extends State { widget.conversation.admin ? const SizedBox(height: 25) : const SizedBox.shrink(), - sectionTitle('Members'), + sectionTitle('Members', showUsersAdd: true), usersList(), const SizedBox(height: 25), myAccess(), @@ -112,9 +116,22 @@ class _ConversationSettingsState extends State { onPressed: () { Navigator.of(context).push( MaterialPageRoute(builder: (context) => ConversationEditDetails( - users: users, - conversation: widget.conversation, - friends: null, + saveCallback: (String conversationName) async { + widget.conversation.name = conversationName; + + final db = await getDatabaseConnection(); + db.update( + 'conversations', + widget.conversation.toMap(), + where: 'id = ?', + whereArgs: [widget.conversation.id], + ); + + await updateConversation(widget.conversation, includeUsers: true); + setState(() {}); + Navigator.pop(context); + }, + conversation: widget.conversation, )), ).then(onGoBack); }, @@ -160,16 +177,49 @@ class _ConversationSettingsState extends State { ); } - Widget sectionTitle(String title) { + Widget sectionTitle(String title, { bool showUsersAdd = false}) { return Align( - alignment: Alignment.centerLeft, - child: Container( - padding: const EdgeInsets.only(left: 12), - child: Text( - title, - style: const TextStyle(fontSize: 20), + alignment: Alignment.centerLeft, + child: Padding( + padding: const EdgeInsets.only(right: 6), + child: Row( + children: [ + Expanded( + child: Container( + padding: const EdgeInsets.only(left: 12), + child: Text( + title, + style: const TextStyle(fontSize: 20), + ), + ), ), - ), + !showUsersAdd ? + const SizedBox.shrink() : + IconButton( + icon: const Icon(Icons.add), + padding: const EdgeInsets.all(0), + onPressed: () async { + List friends = await unselectedFriends(); + Navigator.of(context).push( + MaterialPageRoute(builder: (context) => ConversationAddFriendsList( + friends: friends, + saveCallback: (List selectedFriends) async { + addUsersToConversation( + widget.conversation, + selectedFriends, + ); + + await updateConversation(widget.conversation, includeUsers: true); + await getUsers(); + Navigator.pop(context); + }, + )) + ); + }, + ), + ], + ) + ) ); } @@ -236,9 +286,36 @@ class _ConversationSettingsState extends State { ); } + Future> unselectedFriends() async { + final db = await getDatabaseConnection(); + + List notInArgs = []; + for (var user in users) { + notInArgs.add(user.userId); + } + + final List> maps = await db.query( + 'friends', + where: 'friend_id not in (${List.filled(notInArgs.length, '?').join(',')})', + whereArgs: notInArgs, + orderBy: 'username', + ); + + 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'], + asymmetricPublicKey: maps[i]['asymmetric_public_key'], + acceptedAt: maps[i]['accepted_at'], + username: maps[i]['username'], + ); + }); + } + onGoBack(dynamic value) async { nameController.text = widget.conversation.name; - super.initState(); getUsers(); setState(() {}); }