From f56def8624e4f2e5b49aaaa1295ff90cbcfd18a4 Mon Sep 17 00:00:00 2001 From: gbogboadePush <135420608+gbogboadePush@users.noreply.github.com> Date: Tue, 5 Dec 2023 13:19:52 +0100 Subject: [PATCH 01/14] Scalability changes: modify update group methods (#67) * feat: add getGroupInfo function * chore: add deprecation notice to getGroupByName * feat: add getGroupMemberCount function * feat: add getGroupMembers functions * feat: add getAllGroupMembers function * feat: add getGroupMemberStatus function * feat: ad getGroupMembersPublicKeys function * feat: add getAllGroupMembersPublicKeys function * feat: add updateGroupConfig, updateGroupMembers, updateGroupProfile * feat: add addMembers, removeMembers, addAdmins, removeAdmins * fix: only show group requests for members and not creator * Revert "fix: only show group requests for members and not creator" This reverts commit 0268196381ef4d15e4ca0aac57fdd495989c6fb3. * chore: add ConnectedUser extends User --------- Co-authored-by: Gbogboade Ayomide --- example/lib/test_functions/chat/__chats.dart | 3 + .../chat/create_group_public.dart | 4 +- .../chat/update_group_config.dart | 10 ++ .../chat/update_group_members.dart | 11 ++ .../chat/update_group_profile.dart | 12 ++ .../group/chat_room/chat_room_provider.dart | 7 +- .../lib/views/group/create_group_screen.dart | 7 +- example/lib/views/home_screen.dart | 2 + lib/src/chat/chat.dart | 4 + lib/src/chat/src/add_admins.dart | 53 +------ lib/src/chat/src/add_members.dart | 52 +------ lib/src/chat/src/approve_request.dart | 1 - lib/src/chat/src/create_group.dart | 5 +- lib/src/chat/src/helper/crypto.dart | 10 +- lib/src/chat/src/helper/pgp.dart | 8 +- lib/src/chat/src/helper/validators.dart | 39 +++++ lib/src/chat/src/models/group_info_dto.dart | 2 +- .../chat/src/models/update_group_members.dart | 20 +++ lib/src/chat/src/remove_admins.dart | 55 +------ lib/src/chat/src/remove_members.dart | 49 +----- lib/src/chat/src/update_group.dart | 6 +- lib/src/chat/src/update_group_config.dart | 65 ++++++++ lib/src/chat/src/update_group_members.dart | 139 ++++++++++++++++++ lib/src/chat/src/update_group_profile.dart | 85 +++++++++++ lib/src/helpers/src/converters.dart | 21 +++ lib/src/models/src/user_model.dart | 14 +- lib/src/payloads/src/helpers.dart | 1 - lib/src/spaces/src/add_listeners.dart | 2 +- lib/src/spaces/src/add_speakers.dart | 2 +- lib/src/spaces/src/remove_listeners.dart | 2 +- lib/src/spaces/src/remove_speakers.dart | 2 +- lib/src/spaces/src/space.dart | 1 - lib/src/user/src/profile.update_user.dart | 2 - 33 files changed, 474 insertions(+), 222 deletions(-) create mode 100644 example/lib/test_functions/chat/update_group_config.dart create mode 100644 example/lib/test_functions/chat/update_group_members.dart create mode 100644 example/lib/test_functions/chat/update_group_profile.dart create mode 100644 lib/src/chat/src/models/update_group_members.dart create mode 100644 lib/src/chat/src/update_group_config.dart create mode 100644 lib/src/chat/src/update_group_members.dart create mode 100644 lib/src/chat/src/update_group_profile.dart diff --git a/example/lib/test_functions/chat/__chats.dart b/example/lib/test_functions/chat/__chats.dart index 1c65115..11a43b4 100644 --- a/example/lib/test_functions/chat/__chats.dart +++ b/example/lib/test_functions/chat/__chats.dart @@ -24,3 +24,6 @@ export 'get_all_group_members.dart'; export 'get_group_member_status.dart'; export 'get_group_members_public_keys.dart'; export 'get_all_group_members_public_keys.dart'; +export 'update_group_config.dart'; +export 'update_group_profile.dart'; +export 'update_group_members.dart'; diff --git a/example/lib/test_functions/chat/create_group_public.dart b/example/lib/test_functions/chat/create_group_public.dart index 26289a6..eba6fcf 100644 --- a/example/lib/test_functions/chat/create_group_public.dart +++ b/example/lib/test_functions/chat/create_group_public.dart @@ -4,7 +4,7 @@ import 'package:ethers/signers/wallet.dart' as ether; import '../../models/signer.dart'; -void testCreateGroupPublic() async { +Future testCreateGroupPublic() async { final ethersWallet = ether.Wallet.fromPrivateKey( "c41b72d56258e50595baa969eb0949c5cee9926ac55f7bad21fe327236772e0c"); @@ -23,6 +23,6 @@ void testCreateGroupPublic() async { print(result); if (result != null) { - print('testCreateGroup response: ${result}'); + print('testCreateGroup response: ${result.chatId}'); } } diff --git a/example/lib/test_functions/chat/update_group_config.dart b/example/lib/test_functions/chat/update_group_config.dart new file mode 100644 index 0000000..a395c9f --- /dev/null +++ b/example/lib/test_functions/chat/update_group_config.dart @@ -0,0 +1,10 @@ +import 'package:push_restapi_dart/push_restapi_dart.dart'; + +void testUpdateGroupConfig() async { + final result = await updateGroupConfig( + scheduleAt: DateTime.now().add(Duration(hours: 3)), + chatId: + '64434400c9a61256451f025f1f8c7b34d9651af4a03ca063126e137572fb85ca'); + + print(result); +} diff --git a/example/lib/test_functions/chat/update_group_members.dart b/example/lib/test_functions/chat/update_group_members.dart new file mode 100644 index 0000000..03fb811 --- /dev/null +++ b/example/lib/test_functions/chat/update_group_members.dart @@ -0,0 +1,11 @@ +import 'package:push_restapi_dart/push_restapi_dart.dart'; + +void testUpdateGroupMembers() async { + final result = await updateGroupMembers( + chatId: + 'a279ff975a104e8b04806bee18edc2c489b619663d6b192cbb07b4bbfd616e45', + upsert: + UpsertDTO(admins: ['0x29b8276AA5bc432e03745eF275ded9074faB5970'])); + + print(result); +} diff --git a/example/lib/test_functions/chat/update_group_profile.dart b/example/lib/test_functions/chat/update_group_profile.dart new file mode 100644 index 0000000..60b7e83 --- /dev/null +++ b/example/lib/test_functions/chat/update_group_profile.dart @@ -0,0 +1,12 @@ +import 'package:push_restapi_dart/push_restapi_dart.dart'; + +void testUpdateGroupProfile() async { + final result = await updateGroupProfile( + groupImage: + 'https://tmpfiles.org/dl/3250024/screenshot2023-11-26at8.51.05pm.png', + chatId: + '64434400c9a61256451f025f1f8c7b34d9651af4a03ca063126e137572fb85ca', + groupName: 'Ayo New name'); + + print(result); +} diff --git a/example/lib/views/group/chat_room/chat_room_provider.dart b/example/lib/views/group/chat_room/chat_room_provider.dart index 9ca23c0..3457efa 100644 --- a/example/lib/views/group/chat_room/chat_room_provider.dart +++ b/example/lib/views/group/chat_room/chat_room_provider.dart @@ -57,10 +57,11 @@ class ChatRoomProvider extends ChangeNotifier { } onRefreshRoom({ - GroupDTO? groupData, + GroupInfoDTO? groupData, }) async { - if (groupData?.chatId == _currentChatid) { - _room.groupInformation = groupData; + if (groupData != null && groupData.chatId == _currentChatid) { + _room.groupInformation = + GroupDTO.fromJson(groupData.toJson()); //groupData; notifyListeners(); } diff --git a/example/lib/views/group/create_group_screen.dart b/example/lib/views/group/create_group_screen.dart index 178be9c..3ac7dda 100644 --- a/example/lib/views/group/create_group_screen.dart +++ b/example/lib/views/group/create_group_screen.dart @@ -3,14 +3,14 @@ import 'package:push_restapi_dart/push_restapi_dart.dart'; import '../../__lib.dart'; -class CreateGroupScreen extends StatefulWidget { +class CreateGroupScreen extends ConsumerStatefulWidget { const CreateGroupScreen({super.key}); @override - State createState() => _CreateGroupScreenState(); + ConsumerState createState() => _CreateGroupScreenState(); } -class _CreateGroupScreenState extends State { +class _CreateGroupScreenState extends ConsumerState { TextEditingController nameController = TextEditingController(); TextEditingController descriptionController = TextEditingController(); TextEditingController memberController = TextEditingController(); @@ -138,6 +138,7 @@ class _CreateGroupScreenState extends State { showMyDialog( context: context, title: 'Error', message: 'Group not created'); } else { + ref.read(conversationsProvider).loadChats(); showMyDialog( context: context, title: 'Success', diff --git a/example/lib/views/home_screen.dart b/example/lib/views/home_screen.dart index 4dd7e49..8e6346f 100644 --- a/example/lib/views/home_screen.dart +++ b/example/lib/views/home_screen.dart @@ -15,6 +15,8 @@ class _HomeScreenState extends ConsumerState { int currentIndex = 0; onCopy() { + testUpdateGroupMembers(); + final pushWallet = ref.read(accountProvider).pushWallet!; FlutterClipboard.copy(pushWallet.address!).then((value) { diff --git a/lib/src/chat/chat.dart b/lib/src/chat/chat.dart index 52d4ef5..d040577 100644 --- a/lib/src/chat/chat.dart +++ b/lib/src/chat/chat.dart @@ -12,6 +12,7 @@ export 'src/models/chat_member_counts.dart'; export 'src/models/chat_members.dart'; export 'src/models/group_member_status.dart'; export 'src/models/group_member_public_key.dart'; +export 'src/models/update_group_members.dart'; export 'src/chat.dart'; export 'src/chats.dart'; @@ -37,3 +38,6 @@ export 'src/get_all_group_members.dart'; export 'src/get_group_member_status.dart'; export 'src/get_group_members_public_keys.dart'; export 'src/get_all_group_members_public_keys.dart'; +export 'src/update_group_config.dart'; +export 'src/update_group_profile.dart'; +export 'src/update_group_members.dart'; diff --git a/lib/src/chat/src/add_admins.dart b/lib/src/chat/src/add_admins.dart index 4067fe2..9268a6f 100644 --- a/lib/src/chat/src/add_admins.dart +++ b/lib/src/chat/src/add_admins.dart @@ -2,7 +2,7 @@ import '../../../push_restapi_dart.dart' as push; import '../../../push_restapi_dart.dart'; -Future addAdmins({ +Future addAdmins({ required String chatId, String? account, Signer? signer, @@ -22,56 +22,13 @@ Future addAdmins({ throw Exception("Admin address array cannot be empty!"); } - for (var admin in admins) { - if (!isValidETHAddress(admin)) { - throw Exception('Invalid admin address: $admin'); - } - } - - final group = await getGroup(chatId: chatId); - - if (group == null) { - throw Exception('Group not found: $chatId'); - } - - List convertedMembers = - getMembersList(group.members, group.pendingMembers); - - List adminsToBeAdded = - admins.map((admin) => walletToPCAIP10(admin)).toList(); - - for (var admin in adminsToBeAdded) { - if (!convertedMembers.contains(admin)) { - convertedMembers.add(admin); - } - } - - List convertedAdmins = - getAdminsList(group.members, group.pendingMembers); - - for (var admin in adminsToBeAdded) { - if (convertedAdmins.contains(admin)) { - throw Exception('Admin $admin already exists in the list'); - } - } - - convertedAdmins.addAll(adminsToBeAdded); - - final updatedGroup = await push.updateGroup( + return push.updateGroupMembers( chatId: chatId, - groupName: group.groupName!, - groupImage: group.groupImage, - groupDescription: group.groupDescription!, - members: convertedMembers, - admins: convertedAdmins, signer: signer, - scheduleAt: group.scheduleAt, - scheduleEnd: group.scheduleEnd, - status: group.status, - isPublic: group.isPublic, + pgpPrivateKey: pgpPrivateKey, + account: account, + upsert: UpsertDTO(admins: admins), ); - - return updatedGroup; } catch (e) { log("[Push SDK] - API - Error - API addAdmins -: $e "); rethrow; diff --git a/lib/src/chat/src/add_members.dart b/lib/src/chat/src/add_members.dart index 63f5684..96fa65a 100644 --- a/lib/src/chat/src/add_members.dart +++ b/lib/src/chat/src/add_members.dart @@ -1,7 +1,7 @@ import '../../../push_restapi_dart.dart'; import 'package:push_restapi_dart/push_restapi_dart.dart' as push; -Future addMembers({ +Future addMembers({ required String chatId, String? account, Signer? signer, @@ -21,49 +21,13 @@ Future addMembers({ throw Exception("Member address array cannot be empty!"); } - for (var member in members) { - if (!isValidETHAddress(member)) { - throw Exception('Invalid member address: $member'); - } - } - - final group = await getGroup(chatId: chatId); - - if (group == null) { - throw Exception('Group not found: $chatId'); - } - - List convertedMembers = - getMembersList(group.members, group.pendingMembers); - - List membersToBeAdded = - members.map((member) => walletToPCAIP10(member)).toList(); - - for (var member in membersToBeAdded) { - if (convertedMembers.contains(member)) { - throw Exception('Member $member already exists in the list'); - } - } - - convertedMembers.addAll(membersToBeAdded); - - List convertedAdmins = - getAdminsList(group.members, group.pendingMembers); - - final updatedGroup = await push.updateGroup( - chatId: chatId, - groupName: group.groupName!, - groupImage: group.groupImage, - groupDescription: group.groupDescription!, - members: convertedMembers, - admins: convertedAdmins, - signer: signer, - scheduleAt: group.scheduleAt, - scheduleEnd: group.scheduleEnd, - status: ChatStatus.ENDED, - isPublic: group.isPublic); - - return updatedGroup; + return push.updateGroupMembers( + chatId: chatId, + signer: signer, + pgpPrivateKey: pgpPrivateKey, + account: account, + upsert: UpsertDTO(members: members), + ); } catch (e) { log("[Push SDK] - API - Error - API addMembers -: $e "); rethrow; diff --git a/lib/src/chat/src/approve_request.dart b/lib/src/chat/src/approve_request.dart index 9c6ea6a..5e14e16 100644 --- a/lib/src/chat/src/approve_request.dart +++ b/lib/src/chat/src/approve_request.dart @@ -44,7 +44,6 @@ Future approve({ final signature = await sign( message: hash, privateKey: pgpPrivateKey, - publicKey: pgpPrivateKey, ); final sigType = "pgp"; diff --git a/lib/src/chat/src/create_group.dart b/lib/src/chat/src/create_group.dart index b21b718..e41677e 100644 --- a/lib/src/chat/src/create_group.dart +++ b/lib/src/chat/src/create_group.dart @@ -1,5 +1,4 @@ import '../../../push_restapi_dart.dart'; -import 'dart:convert'; Future createGroup({ String? account, @@ -61,11 +60,9 @@ Future createGroup({ }; final hash = generateHash(bodyToBeHashed); - final publicKeyJSON = jsonDecode(connectedUser!.user.publicKey!); final signature = await sign( message: hash, - privateKey: connectedUser.privateKey!, - publicKey: publicKeyJSON["key"] ?? connectedUser.user.publicKey!, + privateKey: connectedUser!.privateKey!, ); const sigType = 'pgp'; diff --git a/lib/src/chat/src/helper/crypto.dart b/lib/src/chat/src/helper/crypto.dart index ef03a8b..dfd1142 100644 --- a/lib/src/chat/src/helper/crypto.dart +++ b/lib/src/chat/src/helper/crypto.dart @@ -64,7 +64,9 @@ Future signMessageWithPGP( required String publicKey, required String privateKeyArmored}) async { final signature = await sign( - message: message, publicKey: publicKey, privateKey: privateKeyArmored); + message: message, + privateKey: privateKeyArmored, + ); return signature; } @@ -83,9 +85,9 @@ Future> encryptAndSign({ final encryptedSecret = await pgpEncrypt(plainText: secretKey, keys: keys); final signature = await sign( - message: cipherText, - privateKey: senderPgpPrivateKey, - publicKey: publicKey); + message: cipherText, + privateKey: senderPgpPrivateKey, + ); return { 'cipherText': cipherText, diff --git a/lib/src/chat/src/helper/pgp.dart b/lib/src/chat/src/helper/pgp.dart index 9eb6ec6..c30e980 100644 --- a/lib/src/chat/src/helper/pgp.dart +++ b/lib/src/chat/src/helper/pgp.dart @@ -26,10 +26,10 @@ Future generateKeyPair() async { } } -Future sign( - {required String message, - required String publicKey, - required String privateKey}) async { +Future sign({ + required String message, + required String privateKey, +}) async { final signatureWithVersion = await OpenPGP.sign(message, privateKey, ""); return removeVersionFromPublicKey(signatureWithVersion); } diff --git a/lib/src/chat/src/helper/validators.dart b/lib/src/chat/src/helper/validators.dart index c7631a6..23c4f4c 100644 --- a/lib/src/chat/src/helper/validators.dart +++ b/lib/src/chat/src/helper/validators.dart @@ -100,3 +100,42 @@ void updateGroupRequestValidator( throw Exception('Invalid address field!'); } } + +void validateGroupMemberUpdateOptions({ + required String chatId, + required UpsertDTO upsert, + required List remove, +}) { + if (chatId.isEmpty) { + throw Exception('chatId cannot be null or empty'); + } + + // Validating upsert object + final allowedRoles = ['members', 'admins']; + + upsert.toJson().forEach((role, value) { + if (!allowedRoles.contains(role)) { + throw Exception( + 'Invalid role: $role. Allowed roles are ${allowedRoles.join(', ')}.'); + } + + if (value != null && value is List && value.length > 1000) { + throw Exception('$role array cannot have more than 1000 addresses.'); + } + + value.forEach((address) => { + if (!isValidETHAddress(address)) + {throw Exception('Invalid address found in $role list.')} + }); + }); + + // Validating remove array + if (remove.length > 1000) { + throw Exception('Remove array cannot have more than 1000 addresses.'); + } + for (var address in remove) { + if (!isValidETHAddress(address)) { + throw Exception('Invalid address found in remove list.'); + } + } +} diff --git a/lib/src/chat/src/models/group_info_dto.dart b/lib/src/chat/src/models/group_info_dto.dart index 48f729e..c345e05 100644 --- a/lib/src/chat/src/models/group_info_dto.dart +++ b/lib/src/chat/src/models/group_info_dto.dart @@ -68,8 +68,8 @@ class GroupInfoDTO { 'scheduleAt': scheduleAt?.toIso8601String(), 'scheduleEnd': scheduleEnd?.toIso8601String(), 'groupType': groupType, + 'rules': rules, 'status': chatStringFromChatStatus(status), - 'rules': rules?.toJson(), 'meta': meta, 'sessionKey': sessionKey, 'encryptedSecret': encryptedSecret, diff --git a/lib/src/chat/src/models/update_group_members.dart b/lib/src/chat/src/models/update_group_members.dart new file mode 100644 index 0000000..9197083 --- /dev/null +++ b/lib/src/chat/src/models/update_group_members.dart @@ -0,0 +1,20 @@ +class UpsertDTO { + List members; + List admins; + + UpsertDTO({this.members = const [], this.admins = const []}); + + factory UpsertDTO.fromJson(Map json) { + return UpsertDTO( + admins: json['admins'].cast() ?? [], + members: json['members'].cast() ?? [], + ); + } + + Map toJson() { + final Map data = {}; + data['members'] = members; + data['admins'] = admins; + return data; + } +} diff --git a/lib/src/chat/src/remove_admins.dart b/lib/src/chat/src/remove_admins.dart index deb63df..c56ed7d 100644 --- a/lib/src/chat/src/remove_admins.dart +++ b/lib/src/chat/src/remove_admins.dart @@ -2,7 +2,7 @@ import 'package:push_restapi_dart/push_restapi_dart.dart' as push; import '../../../push_restapi_dart.dart'; -Future removeAdmins({ +Future removeAdmins({ required String chatId, String? account, Signer? signer, @@ -22,57 +22,12 @@ Future removeAdmins({ throw Exception("Admin address array cannot be empty!"); } - for (var admin in admins) { - if (!isValidETHAddress(admin)) { - throw Exception('Invalid admin address: $admin'); - } - } - - final group = await getGroup(chatId: chatId); - - if (group == null) { - throw Exception('Group not found: $chatId'); - } - - List convertedMembers = - getMembersList(group.members, group.pendingMembers); - - List adminsToBeRemoved = - admins.map((admin) => walletToPCAIP10(admin)).toList(); - - for (var admin in adminsToBeRemoved) { - if (!convertedMembers.contains(admin)) { - throw Exception('Member $admin not present in the list'); - } - } - - List convertedAdmins = - getAdminsList(group.members, group.pendingMembers); - - for (var admin in adminsToBeRemoved) { - if (!convertedAdmins.contains(admin)) { - throw Exception('Admin $admin not present in the list'); - } - } - - convertedMembers - .removeWhere((member) => adminsToBeRemoved.contains(member)); - convertedAdmins.removeWhere((member) => adminsToBeRemoved.contains(member)); - - final updatedGroup = await push.updateGroup( + return push.updateGroupMembers( chatId: chatId, - groupName: group.groupName!, - groupImage: group.groupImage, - groupDescription: group.groupDescription!, - members: convertedMembers, - admins: convertedAdmins, signer: signer, - scheduleAt: group.scheduleAt, - scheduleEnd: group.scheduleEnd, - status: ChatStatus.ENDED, - isPublic: group.isPublic); - - return updatedGroup; + pgpPrivateKey: pgpPrivateKey, + account: account, + remove: admins); } catch (e) { log("[Push SDK] - API - Error - API removeAdmins -: $e "); rethrow; diff --git a/lib/src/chat/src/remove_members.dart b/lib/src/chat/src/remove_members.dart index b5725d7..b8bd05a 100644 --- a/lib/src/chat/src/remove_members.dart +++ b/lib/src/chat/src/remove_members.dart @@ -2,7 +2,7 @@ import 'package:push_restapi_dart/push_restapi_dart.dart' as push; import '../../../push_restapi_dart.dart'; -Future removeMembers({ +Future removeMembers({ required String chatId, String? account, Signer? signer, @@ -22,51 +22,12 @@ Future removeMembers({ throw Exception("Member address array cannot be empty!"); } - for (var member in members) { - if (!isValidETHAddress(member)) { - throw Exception('Invalid member address: $member'); - } - } - - final group = await getGroup(chatId: chatId); - - if (group == null) { - throw Exception('Group not found: $chatId'); - } - - List convertedMembers = - getMembersList(group.members, group.pendingMembers); - - List membersToBeRemoved = - members.map((member) => walletToPCAIP10(member)).toList(); - - for (var member in membersToBeRemoved) { - if (!convertedMembers.contains(member)) { - throw Exception('Member $member not present in the list'); - } - } - - convertedMembers = convertedMembers - .where((member) => !membersToBeRemoved.contains(member)) - .toList(); - - List convertedAdmins = - getAdminsList(group.members, group.pendingMembers); - - final updatedGroup = await push.updateGroup( + return push.updateGroupMembers( chatId: chatId, - groupName: group.groupName!, - groupImage: group.groupImage, - groupDescription: group.groupDescription!, - members: convertedMembers, - admins: convertedAdmins, signer: signer, - scheduleAt: group.scheduleAt, - scheduleEnd: group.scheduleEnd, - status: ChatStatus.ENDED, - isPublic: group.isPublic); - - return updatedGroup; + pgpPrivateKey: pgpPrivateKey, + account: account, + remove: members); } catch (e) { log("[Push SDK] - API - Error - API removeMembers -: $e "); rethrow; diff --git a/lib/src/chat/src/update_group.dart b/lib/src/chat/src/update_group.dart index a93e9c8..e34e455 100644 --- a/lib/src/chat/src/update_group.dart +++ b/lib/src/chat/src/update_group.dart @@ -1,5 +1,3 @@ -import 'dart:convert'; - import '../../../push_restapi_dart.dart'; Future updateGroup( @@ -64,11 +62,9 @@ Future updateGroup( final hash = generateHash(bodyToBeHashed); - final publicKeyJSON = jsonDecode(connectedUser!.user.publicKey!); final signature = await sign( message: hash, - privateKey: connectedUser.privateKey!, - publicKey: publicKeyJSON["key"] ?? connectedUser.user.publicKey!, + privateKey: connectedUser!.privateKey!, ); final sigType = 'pgp'; diff --git a/lib/src/chat/src/update_group_config.dart b/lib/src/chat/src/update_group_config.dart new file mode 100644 index 0000000..7976f95 --- /dev/null +++ b/lib/src/chat/src/update_group_config.dart @@ -0,0 +1,65 @@ +import '../../../push_restapi_dart.dart'; + +Future updateGroupConfig({ + String? account, + Signer? signer, + required String chatId, + String? meta, + DateTime? scheduleAt, + DateTime? scheduleEnd, + ChatStatus? status, + String? pgpPrivateKey, +}) async { + account ??= getCachedWallet()?.address; + signer ??= getCachedWallet()?.signer; + pgpPrivateKey ??= getCachedWallet()?.pgpPrivateKey; + + /** + * VALIDATIONS + */ + if (account == null && signer == null) { + throw Exception('At least one from account or signer is necessary!'); + } + + final wallet = getWallet(address: account, signer: signer); + String userDID = getAccountAddress(wallet); + final connectedUser = await getConnectedUserV2( + wallet: wallet, + privateKey: pgpPrivateKey, + ); + /** + * CREATE PROFILE VERIFICATION PROOF + */ + final bodyToBeHashed = { + 'meta': meta, + 'scheduleAt': scheduleAt?.toIso8601String(), + 'scheduleEnd': scheduleEnd?.toIso8601String(), + 'status': status, + }; + final hash = generateHash(bodyToBeHashed); + + final signature = await sign( + message: hash, + privateKey: connectedUser!.privateKey!, + ); + + final sigType = 'pgpv2'; + final configVerificationProof = + '$sigType:$signature:${walletToPCAIP10(userDID)}'; + + final body = { + ...bodyToBeHashed, + 'configVerificationProof': configVerificationProof, + }; + + final result = await http.put( + path: '/v1/chat/groups/$chatId/config', + data: body, + ); + + if (result == null || result is String) { + throw Exception(result); + } + + return GroupDTO.fromJson(result); +} diff --git a/lib/src/chat/src/update_group_members.dart b/lib/src/chat/src/update_group_members.dart new file mode 100644 index 0000000..47463f5 --- /dev/null +++ b/lib/src/chat/src/update_group_members.dart @@ -0,0 +1,139 @@ +import '../../../push_restapi_dart.dart'; + +Future updateGroupMembers({ + String? account, + Signer? signer, + required String chatId, + UpsertDTO? upsert, + List remove = const [], + String? pgpPrivateKey, +}) async { + account ??= getCachedWallet()?.address; + signer ??= getCachedWallet()?.signer; + pgpPrivateKey ??= getCachedWallet()?.pgpPrivateKey; + upsert ??= UpsertDTO(); + + /** + * VALIDATIONS + */ + if (account == null && signer == null) { + throw Exception('At least one from account or signer is necessary!'); + } + + validateGroupMemberUpdateOptions( + chatId: chatId, + upsert: upsert, + remove: remove, + ); + + final wallet = getWallet(address: walletToPCAIP10(account!), signer: signer); + String userDID = getAccountAddress(wallet); + final connectedUser = await getConnectedUserV2( + wallet: wallet, + privateKey: pgpPrivateKey, + ); + + final convertedUpsert = {}; + for (var key in upsert.toJson().keys) { + final userIds = await Future.wait( + List.generate( + (upsert.toJson()[key] ?? []).length, + (index) => getUserDID(address: (upsert!.toJson()[key] ?? [])[index]), + ), + ); + convertedUpsert[key] = userIds; + } + + final convertedRemove = await Future.wait( + List.generate(remove.length, (index) => getUserDID(address: remove[index])), + ); + + String? encryptedSecret; + + final group = await getGroupInfo(chatId: chatId); + if (group == null) { + throw Exception('Group not found'); + } + + print('group.isPublic: ${group.isPublic}'); + print('group.encryptedSecret: ${group.encryptedSecret}'); + + if (!group.isPublic) { + if (group.encryptedSecret != null) { + final isMember = (await getGroupMemberStatus( + chatId: chatId, did: connectedUser!.user.did!)) + .isMember; + + var groupMembers = await getAllGroupMembersPublicKeys(chatId: chatId); + + final removeParticipantSet = + convertedRemove.map((e) => e.toLowerCase()).toSet(); + + bool sameMembers = true; + + for (var member in groupMembers) { + if (removeParticipantSet.contains(member.did.toLowerCase())) { + sameMembers = false; + break; + } + } + + if (!sameMembers || !isMember) { + final secretKey = generateRandomSecret(15); + final publicKeys = []; + + // This will now only take keys of non-removed members + for (var member in groupMembers) { + if (!removeParticipantSet.contains(member.did.toLowerCase())) { + publicKeys.add(member.publicKey); + } + } + + // This is autoJoin Case + if (!isMember) { + publicKeys.add(connectedUser.user.publicKey!); + } + + encryptedSecret = await pgpEncrypt( + plainText: secretKey, + keys: publicKeys, + ); + } + } + } + + final bodyToBeHashed = { + "upsert": convertedUpsert, + "remove": convertedRemove, + "encryptedSecret": encryptedSecret, + }; + + final hash = generateHash(bodyToBeHashed); + + final signature = await sign( + message: hash, + privateKey: connectedUser!.privateKey!, + ); + + final sigType = 'pgpv2'; + final deltaVerificationProof = + '$sigType:$signature:${walletToPCAIP10(userDID)}'; + + final body = { + "upsert": convertedUpsert, + "remove": convertedRemove, + "encryptedSecret": encryptedSecret, + "deltaVerificationProof": deltaVerificationProof, + }; + + final result = await http.put( + path: '/v1/chat/groups/$chatId/members', + data: body, + ); + + if (result == null || result is String) { + throw Exception(result); + } + + return GroupInfoDTO.fromJson(result); +} diff --git a/lib/src/chat/src/update_group_profile.dart b/lib/src/chat/src/update_group_profile.dart new file mode 100644 index 0000000..3376a98 --- /dev/null +++ b/lib/src/chat/src/update_group_profile.dart @@ -0,0 +1,85 @@ +import '../../../push_restapi_dart.dart'; + +Future updateGroupProfile({ + String? account, + Signer? signer, + required String chatId, + required String groupName, + String? groupDescription, + required String groupImage, + dynamic rules, + String? pgpPrivateKey, +}) async { + account ??= getCachedWallet()?.address; + signer ??= getCachedWallet()?.signer; + pgpPrivateKey ??= getCachedWallet()?.pgpPrivateKey; + + /** + * VALIDATIONS + */ + if (account == null && signer == null) { + throw Exception('At least one from account or signer is necessary!'); + } + + final wallet = getWallet(address: walletToPCAIP10(account!), signer: signer); + String userDID = getAccountAddress(wallet); + final connectedUser = await getConnectedUserV2( + wallet: wallet, + privateKey: pgpPrivateKey, + ); + + updateGroupRequestValidator( + chatId, + groupName, + groupDescription ?? '', + groupImage, + [], + [], + userDID, + ); + + final group = await getGroupInfo(chatId: chatId); + if (group == null) { + throw Exception('Group not found'); + } + /** + * CREATE PROFILE VERIFICATION PROOF + */ + final bodyToBeHashed = { + 'groupName': groupName, + 'groupDescription': groupDescription ?? group.groupDescription, + 'groupImage': groupImage, + 'rules': rules ?? {}, + "isPublic": group.isPublic, + "groupType": group.groupType, + }; + final hash = generateHash(bodyToBeHashed); + + final signature = await sign( + message: hash, + privateKey: connectedUser!.privateKey!, + ); + + final sigType = 'pgpv2'; + final profileVerificationProof = + '$sigType:$signature:${walletToPCAIP10(userDID)}'; + + final body = { + 'groupName': groupName, + 'groupDescription': groupDescription ?? group.groupDescription, + 'groupImage': groupImage, + 'rules': rules ?? {}, + 'profileVerificationProof': profileVerificationProof, + }; + + final result = await http.put( + path: '/v1/chat/groups/$chatId/profile', + data: body, + ); + + if (result == null || result is String) { + throw Exception(result); + } + + return GroupInfoDTO.fromJson(result); +} diff --git a/lib/src/helpers/src/converters.dart b/lib/src/helpers/src/converters.dart index d95de9c..bcc9224 100644 --- a/lib/src/helpers/src/converters.dart +++ b/lib/src/helpers/src/converters.dart @@ -61,6 +61,27 @@ SpaceDTO groupDtoToSpaceDto(GroupDTO groupDto) { ); } +SpaceDTO groupInfoDtoToSpaceDto(GroupInfoDTO groupDto) { + return SpaceDTO( + members: [], + pendingMembers: [], + contractAddressERC20: null, + numberOfERC20: -1, + numberOfNFTTokens: -1, + verificationProof: '', + spaceImage: groupDto.groupImage, + spaceName: groupDto.groupName, + isPublic: groupDto.isPublic, + spaceDescription: groupDto.groupDescription, + spaceCreator: groupDto.groupCreator, + spaceId: groupDto.chatId, + scheduleAt: groupDto.scheduleAt, + scheduleEnd: groupDto.scheduleEnd, + status: groupDto.status, + meta: groupDto.meta, + ); +} + List convertToWalletAddressList(List memberList) { return memberList.map((member) => member.wallet).toList(); } diff --git a/lib/src/models/src/user_model.dart b/lib/src/models/src/user_model.dart index 0ce1f56..3f0231a 100644 --- a/lib/src/models/src/user_model.dart +++ b/lib/src/models/src/user_model.dart @@ -34,7 +34,19 @@ class ConnectedUser extends User { ConnectedUser({ required this.user, required this.privateKey, - }); + }) { + super.did = user.did; + super.profile = user.profile; + super.name = user.name; + super.about = user.about; + super.verificationProof = user.verificationProof; + super.publicKey = user.publicKey; + super.msgSent = user.msgSent; + super.maxMsgPersisted = user.maxMsgPersisted; + super.wallets = user.wallets; + super.encryptedPrivateKey = user.encryptedPrivateKey; + + } } class User { diff --git a/lib/src/payloads/src/helpers.dart b/lib/src/payloads/src/helpers.dart index cf5a8c1..a806c0e 100644 --- a/lib/src/payloads/src/helpers.dart +++ b/lib/src/payloads/src/helpers.dart @@ -183,7 +183,6 @@ Future getVerificationProof({ final signature = await sign( message: hash, privateKey: pgpPrivateKey as String, - publicKey: jsonDecode(pgpPublicKey as String)['key'], ); verificationProof = 'pgpv2:$signature:meta:$chatId::uid::$uuid'; break; diff --git a/lib/src/spaces/src/add_listeners.dart b/lib/src/spaces/src/add_listeners.dart index 18e22d8..b0fd535 100644 --- a/lib/src/spaces/src/add_listeners.dart +++ b/lib/src/spaces/src/add_listeners.dart @@ -33,7 +33,7 @@ Future addListeners({ account: account, pgpPrivateKey: pgpPrivateKey); if (group != null) { - return groupDtoToSpaceDto(group); + return groupInfoDtoToSpaceDto(group); } else { throw Exception('Error while updating Space : $spaceId'); } diff --git a/lib/src/spaces/src/add_speakers.dart b/lib/src/spaces/src/add_speakers.dart index ed44bf0..f929051 100644 --- a/lib/src/spaces/src/add_speakers.dart +++ b/lib/src/spaces/src/add_speakers.dart @@ -31,7 +31,7 @@ Future addSpeakers({ account: account, pgpPrivateKey: pgpPrivateKey); if (group != null) { - return groupDtoToSpaceDto(group); + return groupInfoDtoToSpaceDto(group); } else { throw Exception('Error while updating Space : $spaceId'); } diff --git a/lib/src/spaces/src/remove_listeners.dart b/lib/src/spaces/src/remove_listeners.dart index 20ffcc1..2938097 100644 --- a/lib/src/spaces/src/remove_listeners.dart +++ b/lib/src/spaces/src/remove_listeners.dart @@ -32,7 +32,7 @@ Future removeListeners({ account: account, pgpPrivateKey: pgpPrivateKey); if (group != null) { - return groupDtoToSpaceDto(group); + return groupInfoDtoToSpaceDto(group); } else { throw Exception('Error while updating Space : $spaceId'); } diff --git a/lib/src/spaces/src/remove_speakers.dart b/lib/src/spaces/src/remove_speakers.dart index e664051..3fb4aef 100644 --- a/lib/src/spaces/src/remove_speakers.dart +++ b/lib/src/spaces/src/remove_speakers.dart @@ -32,7 +32,7 @@ Future removeSpeakers({ account: account, pgpPrivateKey: pgpPrivateKey); if (group != null) { - return groupDtoToSpaceDto(group); + return groupInfoDtoToSpaceDto(group); } else { throw Exception('Error while updating Space : $spaceId'); } diff --git a/lib/src/spaces/src/space.dart b/lib/src/spaces/src/space.dart index d20aeef..5bf40af 100644 --- a/lib/src/spaces/src/space.dart +++ b/lib/src/spaces/src/space.dart @@ -9,7 +9,6 @@ import 'initialize.dart'; import 'join.dart'; import 'update_space_meta.dart'; import 'stop.dart'; -import 'start.dart'; import '../../../push_restapi_dart.dart'; diff --git a/lib/src/user/src/profile.update_user.dart b/lib/src/user/src/profile.update_user.dart index e5f8192..3cdab6c 100644 --- a/lib/src/user/src/profile.update_user.dart +++ b/lib/src/user/src/profile.update_user.dart @@ -35,11 +35,9 @@ Future profileUpdate({ final hash = generateHash(updatedProfile); - //TODO add sign function parameter values final signature = await sign( message: hash, privateKey: pgpPrivateKey, - publicKey: pgpPrivateKey, ); final sigType = 'pgp'; From c0e9f02e957bc7cd77bff293a0570f51420e11c9 Mon Sep 17 00:00:00 2001 From: gbogboadePush <135420608+gbogboadePush@users.noreply.github.com> Date: Wed, 6 Dec 2023 08:18:06 +0100 Subject: [PATCH 02/14] Scalability changes: add pgpv2 encryption in approve request (#68) * feat: add getGroupInfo function * chore: add deprecation notice to getGroupByName * feat: add getGroupMemberCount function * feat: add getGroupMembers functions * feat: add getAllGroupMembers function * feat: add getGroupMemberStatus function * feat: ad getGroupMembersPublicKeys function * feat: add getAllGroupMembersPublicKeys function * feat: add updateGroupConfig, updateGroupMembers, updateGroupProfile * feat: add addMembers, removeMembers, addAdmins, removeAdmins * fix: only show group requests for members and not creator * Revert "fix: only show group requests for members and not creator" This reverts commit 0268196381ef4d15e4ca0aac57fdd495989c6fb3. * chore: add ConnectedUser extends User * feat: add scalability changes to approve request --------- Co-authored-by: Gbogboade Ayomide --- example/lib/views/chats_tab.dart | 37 +++++++------- .../group/requests/request_provider.dart | 2 +- example/lib/views/home_screen.dart | 2 - lib/src/chat/src/approve_request.dart | 49 +++++++++++++++++-- lib/src/models/src/user_model.dart | 1 - 5 files changed, 65 insertions(+), 26 deletions(-) diff --git a/example/lib/views/chats_tab.dart b/example/lib/views/chats_tab.dart index 0d64406..312c459 100644 --- a/example/lib/views/chats_tab.dart +++ b/example/lib/views/chats_tab.dart @@ -88,24 +88,27 @@ class _ChatRequestsTabState extends ConsumerState { return Center(child: Text('Cannot load Requests')); } - return ListView.separated( - separatorBuilder: (context, index) => Divider(), - itemCount: requestsList.length, - itemBuilder: (context, index) { - final item = requestsList[index]; - final image = - item.groupInformation?.groupImage ?? item.profilePicture; + return RefreshIndicator( + onRefresh: vm.loadRequests, + child: ListView.separated( + separatorBuilder: (context, index) => Divider(), + itemCount: requestsList.length, + itemBuilder: (context, index) { + final item = requestsList[index]; + final image = + item.groupInformation?.groupImage ?? item.profilePicture; - return InkWell( - onTap: () { - onAccetRequests(item.chatId!); - }, - child: ConversationTile( - image: image, - item: item, - subText: 'Tap to Accept Request', - )); - }, + return InkWell( + onTap: () { + onAccetRequests(item.chatId!); + }, + child: ConversationTile( + image: image, + item: item, + subText: 'Tap to Accept Request', + )); + }, + ), ); }, ); diff --git a/example/lib/views/group/requests/request_provider.dart b/example/lib/views/group/requests/request_provider.dart index f2efd9b..d9e0280 100644 --- a/example/lib/views/group/requests/request_provider.dart +++ b/example/lib/views/group/requests/request_provider.dart @@ -16,7 +16,7 @@ class RequestsProvider extends ChangeNotifier { notifyListeners(); } - loadRequests() async { + Future loadRequests() async { setBusy(true); _requests = await requests(toDecrypt: true); diff --git a/example/lib/views/home_screen.dart b/example/lib/views/home_screen.dart index 8e6346f..4dd7e49 100644 --- a/example/lib/views/home_screen.dart +++ b/example/lib/views/home_screen.dart @@ -15,8 +15,6 @@ class _HomeScreenState extends ConsumerState { int currentIndex = 0; onCopy() { - testUpdateGroupMembers(); - final pushWallet = ref.read(accountProvider).pushWallet!; FlutterClipboard.copy(pushWallet.address!).then((value) { diff --git a/lib/src/chat/src/approve_request.dart b/lib/src/chat/src/approve_request.dart index 5e14e16..9270fa5 100644 --- a/lib/src/chat/src/approve_request.dart +++ b/lib/src/chat/src/approve_request.dart @@ -24,6 +24,11 @@ Future approve({ final isGroup = !isValidETHAddress(senderAddress); + final connectedUser = await getConnectedUserV2( + wallet: wallet, + privateKey: pgpPrivateKey, + ); + late String fromDID, toDID; if (isGroup) { @@ -34,19 +39,51 @@ Future approve({ toDID = await getUserDID(address: address); } + String? encryptedSecret; + + /** + * GENERATE VERIFICATION PROOF + */ + + // pgp is used for public grps & w2w + // pgpv2 is used for private grps + var sigType = 'pgp'; + + if (isGroup) { + final group = await getGroupInfo(chatId: senderAddress); + if (!group.isPublic) { + /** + * Secret Key Gen Override has no effect if an encrypted secret key is already present + */ + if (group.encryptedSecret != null) { + sigType = 'pgpv2'; + final secretKey = generateRandomSecret(15); + + final groupMembers = await getAllGroupMembersPublicKeys( + chatId: group.chatId, + ); + + final publickKeys = groupMembers.map((e) => e.publicKey).toList(); + publickKeys.add(connectedUser!.publicKey!); + encryptedSecret = + await pgpEncrypt(plainText: secretKey, keys: publickKeys); + } + } + } + final bodyToBeHashed = { "fromDID": fromDID, "toDID": toDID, "status": status, + if (sigType == 'pgpv2') 'encryptedSecret': encryptedSecret, }; final hash = generateHash(bodyToBeHashed); final signature = await sign( message: hash, - privateKey: pgpPrivateKey, + privateKey: connectedUser!.privateKey!, ); - final sigType = "pgp"; final body = { "fromDID": fromDID, "toDID": toDID, @@ -54,6 +91,7 @@ Future approve({ "status": status, "sigType": sigType, "verificationProof": '$sigType:$signature', + 'encryptedSecret': encryptedSecret, }; final result = await http.put( @@ -62,12 +100,13 @@ Future approve({ skipJsonDecode: true, ); - log('accept result: $result'); - if (result == null) { - return null; + throw Exception(isGroup + ? 'Unanable to accept $senderAddress group invite' + : 'Unable to approve request from $senderAddress'); } + //if chat address was returned if (result is String) { return result; } diff --git a/lib/src/models/src/user_model.dart b/lib/src/models/src/user_model.dart index 3f0231a..f570bcb 100644 --- a/lib/src/models/src/user_model.dart +++ b/lib/src/models/src/user_model.dart @@ -45,7 +45,6 @@ class ConnectedUser extends User { super.maxMsgPersisted = user.maxMsgPersisted; super.wallets = user.wallets; super.encryptedPrivateKey = user.encryptedPrivateKey; - } } From 6c0a3703eda4e2523edc7116922a9661a04d1879 Mon Sep 17 00:00:00 2001 From: Gbogboade Ayomide Date: Mon, 11 Dec 2023 10:47:25 +0100 Subject: [PATCH 03/14] feat: add scalability changes fo updarte group --- .../payload/send_notification.dart | 4 +- .../test_functions/user/test_create_user.dart | 2 +- .../group/chat_room/chat_room_provider.dart | 6 +- lib/src/chat/src/approve_request.dart | 4 +- lib/src/chat/src/create_group.dart | 2 +- lib/src/chat/src/get_group.dart | 2 +- lib/src/chat/src/helper/user.dart | 32 +++-- lib/src/chat/src/update_group.dart | 127 ++++++++++++++---- lib/src/chat/src/update_group_config.dart | 2 +- lib/src/chat/src/update_group_members.dart | 9 +- lib/src/chat/src/update_group_profile.dart | 6 +- lib/src/spaces/src/get_space_by_id.dart | 8 +- lib/src/spaces/src/info.dart | 9 +- lib/src/user/src/create_user.dart | 8 +- lib/src/user/src/get_user.dart | 7 +- 15 files changed, 148 insertions(+), 80 deletions(-) diff --git a/example/lib/test_functions/payload/send_notification.dart b/example/lib/test_functions/payload/send_notification.dart index e1ea26d..b925c54 100644 --- a/example/lib/test_functions/payload/send_notification.dart +++ b/example/lib/test_functions/payload/send_notification.dart @@ -38,7 +38,7 @@ void testSendVideoCallNotification() async { // 5. Decrypt keys in Grp final pgpPrivateKey = await push.decryptPGPKey( - encryptedPGPPrivateKey: user?.encryptedPrivateKey as String, + encryptedPGPPrivateKey: user.encryptedPrivateKey as String, wallet: push.getWallet(signer: signer), ); @@ -47,7 +47,7 @@ void testSendVideoCallNotification() async { senderType: 1, signer: signer, pgpPrivateKey: pgpPrivateKey, - pgpPublicKey: user?.publicKey, + pgpPublicKey: user.publicKey, chatId: group?.chatId, type: push.NOTIFICATION_TYPE.BROADCAST, // broadcast identityType: push.IDENTITY_TYPE.DIRECT_PAYLOAD, // direct payload diff --git a/example/lib/test_functions/user/test_create_user.dart b/example/lib/test_functions/user/test_create_user.dart index 86577a5..b9d9ab6 100644 --- a/example/lib/test_functions/user/test_create_user.dart +++ b/example/lib/test_functions/user/test_create_user.dart @@ -44,7 +44,7 @@ void testCreateRandomUser() async { }, ); - print(result?.did); + print(result.did); } catch (e) { print(e); } diff --git a/example/lib/views/group/chat_room/chat_room_provider.dart b/example/lib/views/group/chat_room/chat_room_provider.dart index 3457efa..b5a2ee5 100644 --- a/example/lib/views/group/chat_room/chat_room_provider.dart +++ b/example/lib/views/group/chat_room/chat_room_provider.dart @@ -208,10 +208,8 @@ class ChatRoomProvider extends ChangeNotifier { Future getLatesGroupInfo() async { final result = await getGroup(chatId: _currentChatid); - if (result != null) { - _room.groupInformation = result; - notifyListeners(); - } + _room.groupInformation = result; + notifyListeners(); } GroupDTO? get groupInformation => _room.groupInformation; diff --git a/lib/src/chat/src/approve_request.dart b/lib/src/chat/src/approve_request.dart index 9270fa5..03548d5 100644 --- a/lib/src/chat/src/approve_request.dart +++ b/lib/src/chat/src/approve_request.dart @@ -64,7 +64,7 @@ Future approve({ ); final publickKeys = groupMembers.map((e) => e.publicKey).toList(); - publickKeys.add(connectedUser!.publicKey!); + publickKeys.add(connectedUser.publicKey!); encryptedSecret = await pgpEncrypt(plainText: secretKey, keys: publickKeys); } @@ -81,7 +81,7 @@ Future approve({ final signature = await sign( message: hash, - privateKey: connectedUser!.privateKey!, + privateKey: connectedUser.privateKey!, ); final body = { diff --git a/lib/src/chat/src/create_group.dart b/lib/src/chat/src/create_group.dart index e41677e..e2ff6ac 100644 --- a/lib/src/chat/src/create_group.dart +++ b/lib/src/chat/src/create_group.dart @@ -62,7 +62,7 @@ Future createGroup({ final hash = generateHash(bodyToBeHashed); final signature = await sign( message: hash, - privateKey: connectedUser!.privateKey!, + privateKey: connectedUser.privateKey!, ); const sigType = 'pgp'; diff --git a/lib/src/chat/src/get_group.dart b/lib/src/chat/src/get_group.dart index ff66a1e..8aeb340 100644 --- a/lib/src/chat/src/get_group.dart +++ b/lib/src/chat/src/get_group.dart @@ -1,6 +1,6 @@ import 'package:push_restapi_dart/push_restapi_dart.dart'; -Future getGroup({required String chatId}) async { +Future getGroup({required String chatId}) async { if (chatId.isEmpty) { throw Exception('chatId cannot be null or empty'); } diff --git a/lib/src/chat/src/helper/user.dart b/lib/src/chat/src/helper/user.dart index ca1483b..2e0e356 100644 --- a/lib/src/chat/src/helper/user.dart +++ b/lib/src/chat/src/helper/user.dart @@ -1,24 +1,38 @@ import 'package:push_restapi_dart/push_restapi_dart.dart'; -Future getConnectedUserV2({ +Future getConnectedUserV2({ + required Wallet wallet, + String? privateKey, +}) async { + return getConnectedUserV2Core(wallet: wallet, privateKey: privateKey); +} + +Future getConnectedUserV2Core({ required Wallet wallet, String? privateKey, }) async { final user = await getUser(address: getAccountAddress(wallet)); - if (user == null) { - return null; - } + if (user != null && user.encryptedPrivateKey != null) { + if (privateKey != null) { + return ConnectedUser(user: user, privateKey: privateKey); + } else { + final decryptedPrivateKey = await getDecryptedPrivateKey( + address: wallet.address!, + wallet: wallet, + user: user, + ); - if (privateKey != null) { - return ConnectedUser(user: user, privateKey: privateKey); + return ConnectedUser(user: user, privateKey: decryptedPrivateKey); + } } else { + final newUser = + await createUser(signer: wallet.signer!, progressHook: (hook) {}); final decryptedPrivateKey = await getDecryptedPrivateKey( address: wallet.address!, wallet: wallet, - user: user, + user: newUser, ); - - return ConnectedUser(user: user, privateKey: decryptedPrivateKey); + return ConnectedUser(user: newUser, privateKey: decryptedPrivateKey); } } diff --git a/lib/src/chat/src/update_group.dart b/lib/src/chat/src/update_group.dart index e34e455..6657332 100644 --- a/lib/src/chat/src/update_group.dart +++ b/lib/src/chat/src/update_group.dart @@ -1,24 +1,58 @@ import '../../../push_restapi_dart.dart'; -Future updateGroup( - {required String chatId, - String? account, - Signer? signer, - required String groupName, - required String groupDescription, - String? groupImage, - required List members, - required List admins, - required bool isPublic, - String? contractAddressNFT, - int? numberOfNFTs, - String? contractAddressERC20, - int? numberOfERC20, - String? pgpPrivateKey, - String? meta, - DateTime? scheduleAt, - DateTime? scheduleEnd, - ChatStatus? status}) async { +Future updateGroup({ + required String chatId, + String? account, + Signer? signer, + required String groupName, + required String groupDescription, + String? groupImage, + required List members, + required List admins, + required bool isPublic, + String? pgpPrivateKey, + String? meta, + DateTime? scheduleAt, + DateTime? scheduleEnd, + ChatStatus? status, + Map? rules, +}) async { + return updateGroupCore( + chatId: chatId, + groupName: groupName, + groupDescription: groupDescription, + members: members, + admins: admins, + isPublic: isPublic, + account: account, + groupImage: groupImage, + meta: meta, + pgpPrivateKey: pgpPrivateKey, + rules: rules, + scheduleAt: scheduleAt, + scheduleEnd: scheduleEnd, + signer: signer, + status: status, + ); +} + +Future updateGroupCore({ + required String chatId, + String? account, + Signer? signer, + required String groupName, + required String groupDescription, + String? groupImage, + required List members, + required List admins, + required bool isPublic, + String? pgpPrivateKey, + String? meta, + DateTime? scheduleAt, + DateTime? scheduleEnd, + ChatStatus? status, + Map? rules, +}) async { try { account ??= getCachedWallet()?.address; signer ??= getCachedWallet()?.signer; @@ -46,17 +80,56 @@ Future updateGroup( privateKey: pgpPrivateKey, ); - final convertedMembersDIDList = + final convertedMembers = await Future.wait(members.map((item) => getUserDID(address: item))); - final convertedAdminsDIDList = + final convertedAdmins = await Future.wait(admins.map((item) => getUserDID(address: item))); + final groupChat = await getGroup(chatId: chatId); + + // Compare members array with updateGroup.members array. If they have all the same elements then return true + final updatedParticipants = + Set.from(convertedAdmins.map((e) => e.toLowerCase())); + + final participantStatus = + await getGroupMemberStatus(chatId: chatId, did: connectedUser.did!); + + var sameMembers = true; + + for (var member in groupChat.members) { + if (!updatedParticipants.contains(member.wallet.toLowerCase())) { + sameMembers = false; + } + } + + String? encryptedSecret; + if ((!sameMembers || !participantStatus.isMember) && !groupChat.isPublic) { + final secretKey = generateRandomSecret(15); + var publicKeys = []; + + // This will now only take keys of non-removed members + for (var member in groupChat.members) { + if (updatedParticipants.contains(member.wallet.toLowerCase())) { + publicKeys.add(member.publicKey!); + } + } + + // This is autoJoin Case + if (!participantStatus.isMember) { + publicKeys.add(connectedUser.publicKey!); + } + + // Encrypt secret key with group members public keys + encryptedSecret = + await pgpEncrypt(plainText: secretKey, keys: publicKeys); + } + final bodyToBeHashed = { 'groupName': groupName, 'groupDescription': groupDescription, 'groupImage': groupImage, - 'members': convertedMembersDIDList, - 'admins': convertedAdminsDIDList, + 'members': convertedMembers, + 'admins': convertedAdmins, 'chatId': chatId, }; @@ -64,7 +137,7 @@ Future updateGroup( final signature = await sign( message: hash, - privateKey: connectedUser!.privateKey!, + privateKey: connectedUser.privateKey!, ); final sigType = 'pgp'; @@ -74,14 +147,16 @@ Future updateGroup( 'groupName': groupName, 'groupImage': groupImage, 'groupDescription': groupDescription, - 'members': convertedMembersDIDList, - 'admins': convertedAdminsDIDList, + 'members': convertedMembers, + 'admins': convertedAdmins, 'address': 'eip155:$userDID', 'verificationProof': verificationProof, + 'encryptedSecret': encryptedSecret, 'scheduleAt': scheduleAt?.toIso8601String(), 'scheduleEnd': scheduleEnd?.toIso8601String(), if (meta != null) 'meta': meta, if (status != null) 'status': chatStringFromChatStatus(status), + if (rules != null) 'rules': rules, }; final result = await http.put( diff --git a/lib/src/chat/src/update_group_config.dart b/lib/src/chat/src/update_group_config.dart index 7976f95..858584f 100644 --- a/lib/src/chat/src/update_group_config.dart +++ b/lib/src/chat/src/update_group_config.dart @@ -40,7 +40,7 @@ Future updateGroupConfig({ final signature = await sign( message: hash, - privateKey: connectedUser!.privateKey!, + privateKey: connectedUser.privateKey!, ); final sigType = 'pgpv2'; diff --git a/lib/src/chat/src/update_group_members.dart b/lib/src/chat/src/update_group_members.dart index 47463f5..6586513 100644 --- a/lib/src/chat/src/update_group_members.dart +++ b/lib/src/chat/src/update_group_members.dart @@ -51,17 +51,12 @@ Future updateGroupMembers({ String? encryptedSecret; final group = await getGroupInfo(chatId: chatId); - if (group == null) { - throw Exception('Group not found'); - } - print('group.isPublic: ${group.isPublic}'); - print('group.encryptedSecret: ${group.encryptedSecret}'); if (!group.isPublic) { if (group.encryptedSecret != null) { final isMember = (await getGroupMemberStatus( - chatId: chatId, did: connectedUser!.user.did!)) + chatId: chatId, did: connectedUser.user.did!)) .isMember; var groupMembers = await getAllGroupMembersPublicKeys(chatId: chatId); @@ -112,7 +107,7 @@ Future updateGroupMembers({ final signature = await sign( message: hash, - privateKey: connectedUser!.privateKey!, + privateKey: connectedUser.privateKey!, ); final sigType = 'pgpv2'; diff --git a/lib/src/chat/src/update_group_profile.dart b/lib/src/chat/src/update_group_profile.dart index 3376a98..9d0c79d 100644 --- a/lib/src/chat/src/update_group_profile.dart +++ b/lib/src/chat/src/update_group_profile.dart @@ -39,9 +39,7 @@ Future updateGroupProfile({ ); final group = await getGroupInfo(chatId: chatId); - if (group == null) { - throw Exception('Group not found'); - } + /** * CREATE PROFILE VERIFICATION PROOF */ @@ -57,7 +55,7 @@ Future updateGroupProfile({ final signature = await sign( message: hash, - privateKey: connectedUser!.privateKey!, + privateKey: connectedUser.privateKey!, ); final sigType = 'pgpv2'; diff --git a/lib/src/spaces/src/get_space_by_id.dart b/lib/src/spaces/src/get_space_by_id.dart index ab35f5d..d0bcd4a 100644 --- a/lib/src/spaces/src/get_space_by_id.dart +++ b/lib/src/spaces/src/get_space_by_id.dart @@ -6,13 +6,9 @@ Future getSpaceById({ }) async { try { final group = await push.getGroup(chatId: spaceId); - if (group != null) { - return groupDtoToSpaceDto(group); - } else { - throw Exception('Space not found for id : $spaceId'); - } + return groupDtoToSpaceDto(group); } catch (e) { print('[Push SDK] - API - Error - API update -: $e'); - rethrow; + throw Exception('Space not found for id : $spaceId'); } } diff --git a/lib/src/spaces/src/info.dart b/lib/src/spaces/src/info.dart index c30f092..e5d273b 100644 --- a/lib/src/spaces/src/info.dart +++ b/lib/src/spaces/src/info.dart @@ -8,13 +8,10 @@ Future info({ throw Exception("spaceId cannot be null or empty"); } final group = await getGroup(chatId: spaceId); - if (group != null) { - return groupDtoToSpaceDto(group); - } else { - throw Exception('Space not found for id : $spaceId'); - } + return groupDtoToSpaceDto(group); } catch (e) { print('[Push SDK] - API - Error - API update -: $e'); - rethrow; + + throw Exception('Space not found for id : $spaceId'); } } diff --git a/lib/src/user/src/create_user.dart b/lib/src/user/src/create_user.dart index cb7a02a..38164d3 100644 --- a/lib/src/user/src/create_user.dart +++ b/lib/src/user/src/create_user.dart @@ -2,7 +2,7 @@ import 'dart:convert'; import '../../../push_restapi_dart.dart'; -Future createUser({ +Future createUser({ required Signer signer, String version = ENCRYPTION_TYPE.PGP_V3, required Function(ProgressHookType) progressHook, @@ -48,10 +48,8 @@ Future createUser({ final result = await http.post(path: '/v2/users', data: data); - if (result == null) { - return null; - } else if (result is String) { - throw Exception(result); + if (result == null || result is String) { + throw Exception(result ?? 'Unable to create account for $address'); } else { return User.fromJson(result); } diff --git a/lib/src/user/src/get_user.dart b/lib/src/user/src/get_user.dart index 8e17f9d..3ba22c7 100644 --- a/lib/src/user/src/get_user.dart +++ b/lib/src/user/src/get_user.dart @@ -10,12 +10,9 @@ Future getUser({ final caip10 = walletToPCAIP10(address); final requestUrl = '/v2/users/?caip10=$caip10'; final result = await http.get(path: requestUrl); - if (result == null || result.isEmpty) { - return null; - } - if (result is String) { - throw Exception("Unable to get user"); + if (result == null || result is String) { + throw Exception(result ?? "Unable to get user with address $address"); } return User.fromJson(result); From 31afa6ee754d238860f447d645a200d774094f83 Mon Sep 17 00:00:00 2001 From: Madhur Gupta Date: Wed, 13 Dec 2023 15:59:13 +0530 Subject: [PATCH 04/14] Delete .DS_Store --- .DS_Store | Bin 6148 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 .DS_Store diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index a561555582c1bda99af05b72dcb1b94110fe2a56..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHKJxc>Y5S>jf7_qt1@_vCDL2S(4u(z5A_wbng8&s;>y8 zCF;=?ol-_48d5LFZa!*d55I={?c+%ebNgiZtdhlEK|QrYuBVDB2Gj}bc-xv;{QQsc z&7N=|aem&iHOLpeaK4DfVBlhociMROZGO({hucZcX{?|1D|?Wa>^V8NiZY-K{BH)( zvsvmZ4s})rlmTU6$^d^K5)8&vuyANU9hmGD02o1S1?!j>z(g<=EF8iDF)kG7LXBK8 zj0=Z85x-QhaOlFx$mPRGW+OKgW3yxb#JZD79qOzMC<7q_RreTk|KDAF{||%oNf}TE z{uBcyYPDNU97%d>>2Ta@6O0oK7WOL~PD8M;tr))CidQkLK%ejem Date: Wed, 13 Dec 2023 11:50:30 +0100 Subject: [PATCH 05/14] Implement scalability changes in encryption and decryption of messages (#70) * feat: implement scalability changes for send message * chore: remove unused code for update group profile * chore: add scalability changes for decrypt --------- Co-authored-by: Gbogboade Ayomide --- .../lib/test_functions/chat/fetch_chats.dart | 8 +- example/lib/test_functions/chat/send.dart | 4 +- .../test_functions/chat/send_composite.dart | 5 +- .../lib/test_functions/chat/send_file.dart | 12 +- .../lib/test_functions/chat/send_group.dart | 5 +- .../test_functions/chat/send_group_meta.dart | 14 +- .../lib/test_functions/chat/send_image.dart | 5 +- .../test_functions/chat/send_media_embed.dart | 5 +- .../test_functions/chat/send_reaction.dart | 4 +- .../lib/test_functions/chat/send_reply.dart | 4 +- .../space/send_live_space_data_meta.dart | 5 +- .../space/send_live_space_data_ua.dart | 5 +- .../space/send_space_reaction.dart | 5 +- .../group/chat_room/chat_room_provider.dart | 6 +- lib/src/chat/chat.dart | 2 + lib/src/chat/src/chats.dart | 10 +- lib/src/chat/src/helper/crypto.dart | 160 +++++++----- lib/src/chat/src/helper/inbox.dart | 6 +- lib/src/chat/src/helper/payload_helper.dart | 68 +++++ lib/src/chat/src/helper/pgp.dart | 16 +- lib/src/chat/src/helper/validators.dart | 25 ++ lib/src/chat/src/models/send_models.dart | 76 ++++++ lib/src/chat/src/send.dart | 239 +++--------------- lib/src/chat/src/update_group_members.dart | 6 - lib/src/chat/src/update_group_profile.dart | 3 - lib/src/helpers/helpers.dart | 1 + lib/src/helpers/src/crypto.dart | 155 +++++++----- lib/src/helpers/src/get_encrypted_secret.dart | 15 ++ lib/src/models/src/message.dart | 8 + .../src/helpers/send_live_space_data.dart | 5 +- lib/src/spaces/src/space.dart | 2 +- lib/src/user/src/create_user.dart | 6 +- 32 files changed, 488 insertions(+), 402 deletions(-) create mode 100644 lib/src/chat/src/helper/payload_helper.dart create mode 100644 lib/src/chat/src/models/send_models.dart create mode 100644 lib/src/helpers/src/get_encrypted_secret.dart diff --git a/example/lib/test_functions/chat/fetch_chats.dart b/example/lib/test_functions/chat/fetch_chats.dart index 155b8a3..b1e963a 100644 --- a/example/lib/test_functions/chat/fetch_chats.dart +++ b/example/lib/test_functions/chat/fetch_chats.dart @@ -25,10 +25,10 @@ testAES() { testSign() async { try { - final result = await signMessageWithPGP( - message: 'message', - publicKey: '', - privateKeyArmored: 'privateKeyArmored'); + final result = await signMessageWithPGPCore( + message: 'message', + privateKeyArmored: 'privateKeyArmored', + ); print('testSign: result $result'); } catch (e) { diff --git a/example/lib/test_functions/chat/send.dart b/example/lib/test_functions/chat/send.dart index 736f819..e467aba 100644 --- a/example/lib/test_functions/chat/send.dart +++ b/example/lib/test_functions/chat/send.dart @@ -31,10 +31,10 @@ void testSend() async { print('pgpPrivateKey: $pgpPrivateKey'); final options = ChatSendOptions( - accountAddress: ethersWallet.address, + account: ethersWallet.address, pgpPrivateKey: pgpPrivateKey, messageContent: 'Testing send() from Dart SDK for w2w chat', - receiverAddress: '0x69e666767Ba3a661369e1e2F572EdE7ADC926029', + to: '0x69e666767Ba3a661369e1e2F572EdE7ADC926029', ); final result = await send(options); diff --git a/example/lib/test_functions/chat/send_composite.dart b/example/lib/test_functions/chat/send_composite.dart index e2ead7d..7699131 100644 --- a/example/lib/test_functions/chat/send_composite.dart +++ b/example/lib/test_functions/chat/send_composite.dart @@ -28,14 +28,13 @@ Future testSendComposite() async { } final options = ChatSendOptions( - accountAddress: ethersWallet.address, + account: ethersWallet.address, pgpPrivateKey: pgpPrivateKey, message: CompositeMessage(content: [ NestedContent(type: MessageType.TEXT, content: "inner message 1"), NestedContent(type: MessageType.TEXT, content: "inner message 2") ]), - receiverAddress: - '83e6aaf9fb44c5929ea965d2b0c4e98fd8b6094b72f51989123f81e6cf69f298', + to: '83e6aaf9fb44c5929ea965d2b0c4e98fd8b6094b72f51989123f81e6cf69f298', ); final result = await send(options); diff --git a/example/lib/test_functions/chat/send_file.dart b/example/lib/test_functions/chat/send_file.dart index bd89d80..908bbb4 100644 --- a/example/lib/test_functions/chat/send_file.dart +++ b/example/lib/test_functions/chat/send_file.dart @@ -28,14 +28,14 @@ Future testSendFile() async { } final options = ChatSendOptions( - accountAddress: ethersWallet.address, + account: ethersWallet.address, pgpPrivateKey: pgpPrivateKey, message: FileMessage( - content: "data:text/plain;base64,VmVuZ2VhbmNlMjM0NUAjJCU=", - name: "something.txt", - type: "text/plain"), - receiverAddress: - '83e6aaf9fb44c5929ea965d2b0c4e98fd8b6094b72f51989123f81e6cf69f298', + content: "data:text/plain;base64,VmVuZ2VhbmNlMjM0NUAjJCU=", + name: "something.txt", + type: "text/plain", + ), + to: '83e6aaf9fb44c5929ea965d2b0c4e98fd8b6094b72f51989123f81e6cf69f298', ); final result = await send(options); diff --git a/example/lib/test_functions/chat/send_group.dart b/example/lib/test_functions/chat/send_group.dart index 7903ffb..29196b3 100644 --- a/example/lib/test_functions/chat/send_group.dart +++ b/example/lib/test_functions/chat/send_group.dart @@ -31,11 +31,10 @@ Future testSendToGroup() async { print('pgpPrivateKey: $pgpPrivateKey'); final options = ChatSendOptions( - accountAddress: ethersWallet.address, + account: ethersWallet.address, pgpPrivateKey: pgpPrivateKey, messageContent: 'Testing send() from Dart SDK for group chat', - receiverAddress: - '83e6aaf9fb44c5929ea965d2b0c4e98fd8b6094b72f51989123f81e6cf69f298', + to: '83e6aaf9fb44c5929ea965d2b0c4e98fd8b6094b72f51989123f81e6cf69f298', ); final result = await send(options); diff --git a/example/lib/test_functions/chat/send_group_meta.dart b/example/lib/test_functions/chat/send_group_meta.dart index 490ac3f..44bca3a 100644 --- a/example/lib/test_functions/chat/send_group_meta.dart +++ b/example/lib/test_functions/chat/send_group_meta.dart @@ -28,14 +28,16 @@ Future testSendToGroupMeta() async { } final options = ChatSendOptions( - accountAddress: ethersWallet.address, + account: ethersWallet.address, pgpPrivateKey: pgpPrivateKey, message: MetaMessage( - info: Info( - affected: [ethersWallet.address!], arbitrary: {'key': 'value'}), - content: CHAT.META_GROUP_CREATE), - receiverAddress: - '83e6aaf9fb44c5929ea965d2b0c4e98fd8b6094b72f51989123f81e6cf69f298', + info: Info( + affected: [ethersWallet.address!], + arbitrary: {'key': 'value'}, + ), + content: CHAT.META_GROUP_CREATE, + ), + to: '83e6aaf9fb44c5929ea965d2b0c4e98fd8b6094b72f51989123f81e6cf69f298', ); final result = await send(options); diff --git a/example/lib/test_functions/chat/send_image.dart b/example/lib/test_functions/chat/send_image.dart index d4411aa..f7fa04b 100644 --- a/example/lib/test_functions/chat/send_image.dart +++ b/example/lib/test_functions/chat/send_image.dart @@ -28,15 +28,14 @@ Future testSendImage() async { } final options = ChatSendOptions( - accountAddress: ethersWallet.address, + account: ethersWallet.address, pgpPrivateKey: pgpPrivateKey, message: ImageMessage( name: "something.png", content: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAARMAAAC3CAMAAAAGjUrGAAAAjVBMVEX///+8AC27ACq6ACC6ACK7ACe6AB65ABu+AC+8ACi7ACX88/W3AAC4ABP77/K5ABf23eTy0NfwytLrwcjimqnXdInLVmjFQFTBI0G/CTTtvcjclKC4AA756OzPU23XeYvEMUvKSWHTaX6/FDjosLvZgZHFNE/z1dzlqLTQYHXKQV7orLrWfIrPVm/nvMFRtU1mAAADLklEQVR42u3d2XKjMBCFYbcQBuQFEydesGNDvGT3+z/eXEzNzaCpyQJKNf6/RziFpHZby2AAAAAAAAAAAAAAAAAAAAAGg8FoVtzM55fLZT6/KWaja09jWtzeLZardZrZJE9slq5Xy8XdbTG90mRG5Wax3iaRy4wx8psxJnNRsl0vNuXVxXJ/2eXDofuTxd+MGw7z3eX+ihIp9tXQyf+4YbUvriSRst5GRj7CRNu6vIZZpLJOPs7Zqu8zS1knsXxOnPT6W5k9jJ18nhs/zPo6bDaHSL4mOmx6OYDudxMjX2Umux4uzEdr5TusPfZt3Jyske8x9tSr8TOqE/m+pO5RKLMqkjZEVW/Wn+JspR323JNi/9E5aYtzj734StaZtCdbF32YS5y0yemfU6YrK+2yq6nySHaRtC3a6Q5ln0j7kr3mSJ66iEQkeVI8v4rpJBMjaufZUW2lG1ZtlX/KpSv5SenIiU1nmZhY5+h5ttId+6yyiTSRLk0Utpim57jTTOKzvsrtaSjdGuorUlLTcSYmZTZRP6OMlnHnmcRLXYXbPJfu5XNqE901yuxgAmRiDpqK2c4XYoXL8YsLkol7UVTDjiWMsZ5a9jUPlEn+qiaTtyhQJtGbmqFTx4EyiWstg2eWmkCZmFTLalxOJJSJlu1/pyhYJpGWvuy7C5aJe1eSSWWCZWIqJX2CQ8BMDjr6BUUaMJNUx3aUMpNwMh0LT+kCZuJ0ZHK0ATNRspP4GAXMJNKRSaCGkqq2UkcbcVRvz+E7YT5h3aE+oY7l9w6/i+mf0GejH6uyH0vfnv93PoL/AT34v7iJfQWewcP+kyb2Kf1YW0nVfjb2PXqwP7aJfdSefgH77T1NWc5lNHF+5weWY4XnvDgPGH5Gmai8mozzxZ5ilnPonr4s9xU0CzfutfCMHu4/8RQp3JPTxH1KnsqNe7c8oXA/m2ee5R6/Ju579OBeUN+Xwv2xvjmFe4Y9VT73UTdD4d5yX4uJ++2beAfBN354L8O3/vCuigfv7/gGEO80+b8V3vPyFPu8++ZbmHkf0D+z8I6kLxbeG/1XMrxLCwAAAAAAAAAAAAAAAABA0y/RclUO6yXgjgAAAABJRU5ErkJggg==", ), - receiverAddress: - '83e6aaf9fb44c5929ea965d2b0c4e98fd8b6094b72f51989123f81e6cf69f298', + to: '83e6aaf9fb44c5929ea965d2b0c4e98fd8b6094b72f51989123f81e6cf69f298', ); final result = await send(options); diff --git a/example/lib/test_functions/chat/send_media_embed.dart b/example/lib/test_functions/chat/send_media_embed.dart index b77826c..6fad06b 100644 --- a/example/lib/test_functions/chat/send_media_embed.dart +++ b/example/lib/test_functions/chat/send_media_embed.dart @@ -28,14 +28,13 @@ Future testSendMediaEmbed() async { } final options = ChatSendOptions( - accountAddress: ethersWallet.address, + account: ethersWallet.address, pgpPrivateKey: pgpPrivateKey, message: SendMessage( type: MessageType.MEDIA_EMBED, content: "https://media2.giphy.com/media/p0L1rezLH2Tja/giphy.gif?cid=c918c0ff667b3vbiu4i4e5d1t9sqssx8uvy10reprq8yds23&ep=v1_gifs_trending&rid=giphy.gif&ct=g"), - receiverAddress: - '83e6aaf9fb44c5929ea965d2b0c4e98fd8b6094b72f51989123f81e6cf69f298', + to: '83e6aaf9fb44c5929ea965d2b0c4e98fd8b6094b72f51989123f81e6cf69f298', ); final result = await send(options); diff --git a/example/lib/test_functions/chat/send_reaction.dart b/example/lib/test_functions/chat/send_reaction.dart index 1e2768d..3fad870 100644 --- a/example/lib/test_functions/chat/send_reaction.dart +++ b/example/lib/test_functions/chat/send_reaction.dart @@ -46,11 +46,11 @@ Future testSendReaction() async { } final options = ChatSendOptions( - accountAddress: ethersWallet.address, + account: ethersWallet.address, pgpPrivateKey: pgpPrivateKey, message: ReactionMessage( content: CHAT.REACTION_THUMBSUP, reference: referenceLink), - receiverAddress: groupId, + to: groupId, ); final result = await send(options); diff --git a/example/lib/test_functions/chat/send_reply.dart b/example/lib/test_functions/chat/send_reply.dart index 3330e0a..f170577 100644 --- a/example/lib/test_functions/chat/send_reply.dart +++ b/example/lib/test_functions/chat/send_reply.dart @@ -46,14 +46,14 @@ Future testSendReply() async { } final options = ChatSendOptions( - accountAddress: ethersWallet.address, + account: ethersWallet.address, pgpPrivateKey: pgpPrivateKey, message: ReplyMessage( content: NestedContent( type: MessageType.TEXT, content: "reply message sent from the Dart SDK"), reference: referenceLink), - receiverAddress: groupId, + to: groupId, ); final result = await send(options); diff --git a/example/lib/test_functions/space/send_live_space_data_meta.dart b/example/lib/test_functions/space/send_live_space_data_meta.dart index 74dbb3e..4f22648 100644 --- a/example/lib/test_functions/space/send_live_space_data_meta.dart +++ b/example/lib/test_functions/space/send_live_space_data_meta.dart @@ -48,11 +48,10 @@ Future testSendLiveSpaceDataMeta() async { content: content); final options = ChatSendOptions( - accountAddress: ethersWallet.address, + account: ethersWallet.address, pgpPrivateKey: pgpPrivateKey, message: metaMessage, - receiverAddress: - 'spaces:cff80fae9b898b9f5d679bfa7ef4dfcb7d31b9d1c12e032f4ec6d84a575e62cb', + to: 'spaces:cff80fae9b898b9f5d679bfa7ef4dfcb7d31b9d1c12e032f4ec6d84a575e62cb', ); final result = await send(options); diff --git a/example/lib/test_functions/space/send_live_space_data_ua.dart b/example/lib/test_functions/space/send_live_space_data_ua.dart index 330fc5b..9ad566d 100644 --- a/example/lib/test_functions/space/send_live_space_data_ua.dart +++ b/example/lib/test_functions/space/send_live_space_data_ua.dart @@ -48,11 +48,10 @@ Future testSendLiveSpaceDataUA() async { content: content); final options = ChatSendOptions( - accountAddress: ethersWallet.address, + account: ethersWallet.address, pgpPrivateKey: pgpPrivateKey, message: userActivityMessage, - receiverAddress: - 'spaces:cff80fae9b898b9f5d679bfa7ef4dfcb7d31b9d1c12e032f4ec6d84a575e62cb', + to: 'spaces:cff80fae9b898b9f5d679bfa7ef4dfcb7d31b9d1c12e032f4ec6d84a575e62cb', ); final result = await send(options); diff --git a/example/lib/test_functions/space/send_space_reaction.dart b/example/lib/test_functions/space/send_space_reaction.dart index ac046cb..bf6399a 100644 --- a/example/lib/test_functions/space/send_space_reaction.dart +++ b/example/lib/test_functions/space/send_space_reaction.dart @@ -35,11 +35,10 @@ Future testSendSpaceReaction() async { content: content); final options = ChatSendOptions( - accountAddress: ethersWallet.address, + account: ethersWallet.address, pgpPrivateKey: pgpPrivateKey, message: reactionMessage, - receiverAddress: - 'spaces:cff80fae9b898b9f5d679bfa7ef4dfcb7d31b9d1c12e032f4ec6d84a575e62cb', + to: 'spaces:cff80fae9b898b9f5d679bfa7ef4dfcb7d31b9d1c12e032f4ec6d84a575e62cb', ); final result = await send(options); diff --git a/example/lib/views/group/chat_room/chat_room_provider.dart b/example/lib/views/group/chat_room/chat_room_provider.dart index 3457efa..8af1b08 100644 --- a/example/lib/views/group/chat_room/chat_room_provider.dart +++ b/example/lib/views/group/chat_room/chat_room_provider.dart @@ -156,14 +156,14 @@ class ChatRoomProvider extends ChangeNotifier { options = ChatSendOptions( message: messageAttachment, messageContent: content, - receiverAddress: currentChatId, + to: currentChatId, ); } else { options = ChatSendOptions( message: ReplyMessage( content: NestedContent(type: messageType, content: content), reference: jsonEncode(replyTo!.toJson())), - receiverAddress: currentChatId, + to: currentChatId, ); } @@ -196,7 +196,7 @@ class ChatRoomProvider extends ChangeNotifier { updateSending(true); final message = await send(options); updateSending(false); - print('onSendMessage...5..after send..${message?.toJson()}'); + if (message != null) { getRoomMessages(); } diff --git a/lib/src/chat/chat.dart b/lib/src/chat/chat.dart index d040577..8da606d 100644 --- a/lib/src/chat/chat.dart +++ b/lib/src/chat/chat.dart @@ -6,6 +6,7 @@ export 'src/helper/validators.dart'; export 'src/helper/user.dart'; export 'src/helper/service.dart'; export 'src/helper/signature.dart'; +export 'src/helper/payload_helper.dart'; export 'src/models/group_info_dto.dart'; export 'src/models/chat_member_counts.dart'; @@ -13,6 +14,7 @@ export 'src/models/chat_members.dart'; export 'src/models/group_member_status.dart'; export 'src/models/group_member_public_key.dart'; export 'src/models/update_group_members.dart'; +export 'src/models/send_models.dart'; export 'src/chat.dart'; export 'src/chats.dart'; diff --git a/lib/src/chat/src/chats.dart b/lib/src/chat/src/chats.dart index b9b5d77..672a41a 100644 --- a/lib/src/chat/src/chats.dart +++ b/lib/src/chat/src/chats.dart @@ -23,13 +23,6 @@ Future?> chats({ } pgpPrivateKey ??= getCachedWallet()?.pgpPrivateKey; - if (toDecrypt && pgpPrivateKey == null) { - throw Exception('Private Key is required.'); - } - - if (!isValidETHAddress(accountAddress)) { - throw Exception('Invalid address!'); - } try { final userDID = await getUserDID(address: accountAddress); @@ -43,8 +36,7 @@ Future?> chats({ final chatList = (result['chats'] as List).map((e) => Feeds.fromJson(e)).toList(); - final updatedChats = chatList; - addDeprecatedInfo(chatList); + final updatedChats = addDeprecatedInfo(chatList); final feedWithInbox = await getInboxList( feedsList: updatedChats, diff --git a/lib/src/chat/src/helper/crypto.dart b/lib/src/chat/src/helper/crypto.dart index dfd1142..7a02e96 100644 --- a/lib/src/chat/src/helper/crypto.dart +++ b/lib/src/chat/src/helper/crypto.dart @@ -1,6 +1,7 @@ // ignore_for_file: constant_identifier_names import '../../../../push_restapi_dart.dart'; +import 'pgp.dart' as pgphelper; const SIG_TYPE_V2 = 'eip712v2'; @@ -9,27 +10,30 @@ Future> decryptFeeds({ required User connectedUser, required String pgpPrivateKey, }) async { - // User? otherPeer; - // String? - // signatureValidationPubliKey; // To do signature verification, it depends on who has sent the message + User? otherPeer; + String? + signatureValidationPubliKey; // To do signature verification, it depends on who has sent the message for (final feed in feeds) { final message = feed.msg!; - // bool gotOtherPeer = false; + bool gotOtherPeer = false; if (message.encType != 'PlainText') { - // if (message.fromCAIP10 != connectedUser.wallets?.split(',')[0]) { - // if (!gotOtherPeer) { - // otherPeer = await getUser(address: message.fromCAIP10); - // gotOtherPeer = true; - // } - // signatureValidationPubliKey = otherPeer!.publicKey; - // } else { - // signatureValidationPubliKey = connectedUser.publicKey; - // } + if (message.fromCAIP10 != connectedUser.wallets?.split(',')[0]) { + if (!gotOtherPeer) { + otherPeer = await getUser(address: message.fromCAIP10); + gotOtherPeer = true; + } + signatureValidationPubliKey = otherPeer!.publicKey; + } else { + signatureValidationPubliKey = connectedUser.publicKey; + } feed.msg = await decryptAndVerifyMessage( - message: message, privateKeyArmored: pgpPrivateKey); + pgpPublicKey: signatureValidationPubliKey!, + message: message, + pgpPrivateKey: pgpPrivateKey, + ); } } @@ -47,9 +51,12 @@ Future> decryptSpaceFeeds({ for (var feed in feeds) { final msg = feed.msg!; - if (msg.encType == 'pgp') { + if (msg.encType != 'PlainText') { feed.msg = await decryptAndVerifyMessage( - message: msg, privateKeyArmored: pgpPrivateKey); + pgpPublicKey: connectedUser.publicKey!, + message: msg, + pgpPrivateKey: pgpPrivateKey, + ); updatedFeeds.add(feed); } else { updatedFeeds.add(feed); @@ -59,10 +66,10 @@ Future> decryptSpaceFeeds({ return updatedFeeds; } -Future signMessageWithPGP( - {required String message, - required String publicKey, - required String privateKeyArmored}) async { +Future signMessageWithPGPCore({ + required String message, + required String privateKeyArmored, +}) async { final signature = await sign( message: message, privateKey: privateKeyArmored, @@ -70,11 +77,10 @@ Future signMessageWithPGP( return signature; } -Future> encryptAndSign({ +Future> encryptAndSignCore({ required String plainText, required List keys, required String senderPgpPrivateKey, - required String publicKey, String? secretKey, }) async { secretKey ??= generateRandomSecret(32); @@ -98,67 +104,68 @@ Future> encryptAndSign({ }; } -Future getEncryptedRequest({ +Future getEncryptedRequestCore({ required String receiverAddress, - required String senderPublicKey, - required String senderPgpPrivateKey, + required ConnectedUser senderConnectedUser, required String message, required bool isGroup, + GroupInfoDTO? group, required String secretKey, - GroupDTO? group, }) async { if (!isGroup) { + if (!isValidETHAddress(receiverAddress)) { + throw Exception('Invalid receiver address!'); + } + User? receiverCreatedUser = await getUser(address: receiverAddress); - receiverCreatedUser ??= - await createUserEmpty(accountAddress: receiverAddress); if (receiverCreatedUser != null || receiverCreatedUser?.publicKey != null) { - if (!isValidETHAddress(receiverAddress)) { - throw Exception('Invalid receiver address!'); - } - await createUserService( user: receiverAddress, publicKey: '', encryptedPrivateKey: '', ); + // If the user is being created here, that means that user don't have a PGP keys. So this intent will be in plaintext - final signature = await signMessageWithPGP( + final signature = await signMessageWithPGPCore( message: message, - publicKey: senderPublicKey, - privateKeyArmored: senderPgpPrivateKey, + privateKeyArmored: senderConnectedUser.privateKey!, ); return IEncryptedRequest( - message: message, - encryptionType: 'PlainText', - aesEncryptedSecret: '', - signature: signature); + message: message, + encryptionType: 'PlainText', + aesEncryptedSecret: '', + signature: signature, + ); } else { // It's possible for a user to be created but the PGP keys still not created if (receiverCreatedUser != null && !receiverCreatedUser.publicKey! .contains('-----BEGIN PGP PUBLIC KEY BLOCK-----')) { - final signature = await signMessageWithPGP( + final signature = await signMessageWithPGPCore( message: message, - publicKey: senderPublicKey, - privateKeyArmored: senderPgpPrivateKey, + privateKeyArmored: senderConnectedUser.privateKey!, ); return IEncryptedRequest( - message: message, - encryptionType: 'PlainText', - aesEncryptedSecret: '', - signature: signature); + message: message, + encryptionType: 'PlainText', + aesEncryptedSecret: '', + signature: signature, + ); } else { - final response = await encryptAndSign( - plainText: message, - keys: [receiverCreatedUser!.publicKey!, senderPublicKey], - senderPgpPrivateKey: senderPgpPrivateKey, - publicKey: senderPublicKey, - secretKey: secretKey); + final response = await encryptAndSignCore( + plainText: message, + keys: [ + receiverCreatedUser!.publicKey!, + senderConnectedUser.publicKey! + ], + senderPgpPrivateKey: senderConnectedUser.privateKey!, + secretKey: secretKey, + ); return IEncryptedRequest( message: response['cipherText']!, @@ -169,10 +176,9 @@ Future getEncryptedRequest({ } } else if (group != null) { if (group.isPublic) { - final signature = await signMessageWithPGP( + final signature = await signMessageWithPGPCore( message: message, - publicKey: senderPublicKey, - privateKeyArmored: senderPgpPrivateKey, + privateKeyArmored: senderConnectedUser.privateKey!, ); return IEncryptedRequest( @@ -181,21 +187,43 @@ Future getEncryptedRequest({ aesEncryptedSecret: '', signature: signature); } else { - final publicKeys = - group.members.map((member) => member.publicKey!).toList(); + // Private Groups - final response = await encryptAndSign( + // 1. Private Groups with session keys + if (group.sessionKey != null && group.encryptedSecret != null) { + final cipherText = await aesEncrypt( + plainText: message, + secretKey: secretKey, + ); + + final signature = await pgphelper.sign( + message: cipherText, + privateKey: senderConnectedUser.privateKey!, + ); + return IEncryptedRequest( + message: cipherText, + encryptionType: 'pgpv1:group', + aesEncryptedSecret: '', + signature: signature, + ); + } else { + final members = + await getAllGroupMembersPublicKeys(chatId: group.chatId); + final publicKeys = members.map((e) => e.publicKey).toList(); + + final response = await encryptAndSignCore( plainText: message, keys: publicKeys, - senderPgpPrivateKey: senderPgpPrivateKey, - publicKey: senderPublicKey, - secretKey: secretKey); + senderPgpPrivateKey: senderConnectedUser.privateKey!, + secretKey: secretKey, + ); - return IEncryptedRequest( - message: response['cipherText']!, - encryptionType: 'pgp', - aesEncryptedSecret: response['encryptedSecret']!, - signature: response['signature']!); + return IEncryptedRequest( + message: response['cipherText']!, + encryptionType: 'pgp', + aesEncryptedSecret: response['encryptedSecret']!, + signature: response['signature']!); + } } } else { throw Exception('Error in encryption'); diff --git a/lib/src/chat/src/helper/inbox.dart b/lib/src/chat/src/helper/inbox.dart index 24e160d..ed86c5e 100644 --- a/lib/src/chat/src/helper/inbox.dart +++ b/lib/src/chat/src/helper/inbox.dart @@ -234,7 +234,6 @@ List addDeprecatedInfoToMessages(List chats) { return chats; } -// TODO: Remove this Future> decryptConversation({ required List messages, required User? connectedUser, @@ -243,9 +242,10 @@ Future> decryptConversation({ final updatedMessages = []; for (var msg in messages) { - if (msg.encType == 'pgp') { + if (msg.encType != 'PlainText') { msg = await decryptAndVerifyMessage( - privateKeyArmored: pgpPrivateKey, + pgpPrivateKey: pgpPrivateKey, + pgpPublicKey: connectedUser!.publicKey!, message: msg, ); } diff --git a/lib/src/chat/src/helper/payload_helper.dart b/lib/src/chat/src/helper/payload_helper.dart new file mode 100644 index 0000000..7c33f87 --- /dev/null +++ b/lib/src/chat/src/helper/payload_helper.dart @@ -0,0 +1,68 @@ +import 'dart:convert'; + +import '../../../../push_restapi_dart.dart'; + +Future sendMessagePayloadCore({ + required String receiverAddress, + required ConnectedUser senderConnectedUser, + String? senderAddress, + String? senderPgpPrivateKey, + required String messageType, + required String messageContent, + dynamic messageObj, + GroupInfoDTO? group, + required bool isGroup, +}) async { + late String secretKey; + if (isGroup && group?.encryptedSecret != null && group?.sessionKey != null) { + secretKey = await pgpDecrypt( + cipherText: group!.encryptedSecret!, + privateKeyArmored: senderConnectedUser.privateKey!); + } else { + secretKey = generateRandomSecret(15); + } + + final encryptedMessageContentData = await getEncryptedRequestCore( + receiverAddress: receiverAddress, + message: messageContent, + isGroup: isGroup, + group: group, + secretKey: secretKey, + senderConnectedUser: senderConnectedUser, + ); + + final encryptedMessageContent = encryptedMessageContentData.message; + final deprecatedSignature = + removeVersionFromPublicKey(encryptedMessageContentData.signature); + + final encryptedMessageObjData = await getEncryptedRequestCore( + receiverAddress: receiverAddress, + message: jsonEncode(messageObj?.toJson()), + senderConnectedUser: senderConnectedUser, + isGroup: isGroup, + group: group, + secretKey: secretKey, + ); + + final encryptedMessageObj = encryptedMessageObjData.message; + final encryptionType = encryptedMessageObjData.encryptionType; + final encryptedMessageObjSecret = + removeVersionFromPublicKey(encryptedMessageObjData.aesEncryptedSecret); + + return SendMessagePayload( + fromDID: walletToPCAIP10(senderAddress!), + toDID: !isGroup ? walletToPCAIP10(receiverAddress) : group?.chatId ?? '', + fromCAIP10: walletToPCAIP10(senderAddress), + toCAIP10: !isGroup ? walletToPCAIP10(receiverAddress) : group?.chatId ?? '', + messageContent: encryptedMessageContent, + messageObj: encryptionType == 'PlainText' + ? messageObj?.toJson() + : encryptedMessageObj, + messageType: messageType, + signature: deprecatedSignature, + verificationProof: "pgp:$deprecatedSignature", + encType: encryptionType, + encryptedSecret: encryptedMessageObjSecret, + sigType: "pgp", + ); +} diff --git a/lib/src/chat/src/helper/pgp.dart b/lib/src/chat/src/helper/pgp.dart index c30e980..c7d5eb6 100644 --- a/lib/src/chat/src/helper/pgp.dart +++ b/lib/src/chat/src/helper/pgp.dart @@ -51,7 +51,6 @@ Future pgpDecrypt({ required String cipherText, required String privateKeyArmored, }) async { -//TODO implement pgpDecrypt try { return OpenPGP.decrypt(cipherText, privateKeyArmored, ''); } catch (e) { @@ -59,3 +58,18 @@ Future pgpDecrypt({ rethrow; } } + +Future verifySignature( + {required String messageContent, + required String signatureArmored, + required String publicKeyArmored}) async { + try { + await OpenPGP.verify( + signatureArmored, + messageContent, + publicKeyArmored, + ); + } catch (e) { + log('verifySignature Error ${e.toString()}'); + } +} diff --git a/lib/src/chat/src/helper/validators.dart b/lib/src/chat/src/helper/validators.dart index 23c4f4c..4d9063b 100644 --- a/lib/src/chat/src/helper/validators.dart +++ b/lib/src/chat/src/helper/validators.dart @@ -139,3 +139,28 @@ void validateGroupMemberUpdateOptions({ } } } + +Future validateSendOptions(ComputedOptions options) async { + if (options.account == null && options.signer == null) { + throw Exception('At least one from account or signer is necessary!'); + } + + final wallet = getWallet(address: options.account, signer: options.signer); + + if (!isValidETHAddress(wallet.address!)) { + throw Exception('Invalid address ${wallet.address!}'); + } + + if (options.pgpPrivateKey == null && options.signer == null) { + throw Exception( + "Unable to decrypt keys. Please ensure that either 'signer' or 'pgpPrivateKey' is properly defined."); + } + + if (options.messageType != MessageType.COMPOSITE && + options.messageType != MessageType.REPLY && + options.messageObj?.content.isEmpty) { + throw Exception('Cannot send empty message'); + } + + //TODO implement validateMessageObj +} diff --git a/lib/src/chat/src/models/send_models.dart b/lib/src/chat/src/models/send_models.dart new file mode 100644 index 0000000..6868263 --- /dev/null +++ b/lib/src/chat/src/models/send_models.dart @@ -0,0 +1,76 @@ +import '../../../../push_restapi_dart.dart'; + +class ChatSendOptions { + SendMessage? message; + + /// @deprecated - Use message.content instead + String? messageContent; + + /// @deprecated - Use message.type instead + String? messageType; + + /// Message Receiver's Account ( DID ) + String to; + + /// Message Sender's Account ( DID ) + /// In case account is not provided, it will be derived from signer + String? account; + + /// Message Sender's Signer + /// Used for deriving account if not provided + /// Used for decrypting pgpPrivateKey if not provided + Signer? signer; + + /// Message Sender's decrypted pgp private key + /// Used for signing message + String? pgpPrivateKey; + + ChatSendOptions( + {this.message, + this.messageContent, + this.messageType, + required this.to, + this.account, + this.pgpPrivateKey}) { + assert(MessageType.isValidMessageType( + message?.type ?? messageType ?? MessageType.TEXT)); + } + + Map toJson() { + return { + 'message': message?.toJson(), + 'messageContent': messageContent, + 'messageType': messageType, + 'receiverAddress': to, + 'accountAddress': account, + 'pgpPrivateKey': pgpPrivateKey + }; + } +} + +class ComputedOptions { + String messageType; + dynamic messageObj; + String to; + String? account; + Signer? signer; + String? pgpPrivateKey; + + ComputedOptions( + {required this.messageType, + required this.to, + this.signer, + this.messageObj, + this.account, + this.pgpPrivateKey}); + + Map toJson() { + return { + 'messageType': messageType, + 'messageObj': messageObj, + 'to': to, + 'accountAddress': account, + 'pgpPrivateKey': pgpPrivateKey + }; + } +} diff --git a/lib/src/chat/src/send.dart b/lib/src/chat/src/send.dart index 53c6e55..8001242 100644 --- a/lib/src/chat/src/send.dart +++ b/lib/src/chat/src/send.dart @@ -1,82 +1,41 @@ -import 'dart:convert'; - import '../../../push_restapi_dart.dart'; -class ChatSendOptions { - SendMessage? message; - String? messageContent; - String? messageType; - String receiverAddress; - String? accountAddress; - String? pgpPrivateKey; - - ChatSendOptions( - {this.message, - this.messageContent, - this.messageType, - required this.receiverAddress, - this.accountAddress, - this.pgpPrivateKey}) { - assert(MessageType.isValidMessageType( - message?.type ?? messageType ?? MessageType.TEXT)); - } - - Map toJson() { - return { - 'message': message?.toJson(), - 'messageContent': messageContent, - 'messageType': messageType, - 'receiverAddress': receiverAddress, - 'accountAddress': accountAddress, - 'pgpPrivateKey': pgpPrivateKey - }; - } -} - Future send(ChatSendOptions options) async { - ComputedOptions computedOptions = computeOptions(options); + final computedOptions = computeOptions(options); - computedOptions.accountAddress ??= getCachedWallet()?.address; - if (computedOptions.accountAddress == null) { - throw Exception('Account address is required.'); - } + computedOptions.account ??= getCachedWallet()?.address; + computedOptions.signer ??= getCachedWallet()?.signer; + computedOptions.pgpPrivateKey ??= getCachedWallet()?.pgpPrivateKey; - final isValidGroup = isGroup(computedOptions.to); - final group = - isValidGroup ? await getGroup(chatId: computedOptions.to) : null; + /** + * Validate Input Options + */ + await validateSendOptions(computedOptions); - if (isValidGroup && group == null) { - throw Exception( - 'Invalid receiver. Please ensure \'receiver\' is a valid DID or ChatId in case of Group.'); - } + final wallet = getWallet( + address: computedOptions.account, signer: computedOptions.signer); + final connectedUser = await getConnectedUserV2( + wallet: wallet, + privateKey: options.pgpPrivateKey, + ); + + final isGroup = !isValidETHAddress(computedOptions.to); + final group = isGroup ? await getGroupInfo(chatId: computedOptions.to) : null; final conversationHashResponse = await conversationHash( conversationId: computedOptions.to, - accountAddress: computedOptions.accountAddress!, + accountAddress: computedOptions.account!, ); - if (!isValidETHAddress(computedOptions.accountAddress!)) { - throw Exception('Invalid address ${computedOptions.accountAddress}'); - } - - computedOptions.pgpPrivateKey ??= getCachedWallet()?.pgpPrivateKey; - if (computedOptions.pgpPrivateKey == null) { - throw Exception('Private Key is required.'); - } - - bool isIntent = !isValidGroup && conversationHashResponse == null; - await validateSendOptions(computedOptions); + bool isIntent = !isGroup && conversationHashResponse == null; try { - final senderAcount = - await getUser(address: computedOptions.accountAddress!); + final senderAcount = await getUser(address: computedOptions.account!); if (senderAcount == null) { throw Exception('Cannot get sender account.'); } - final senderPublicKey = getPublicKeyFromString(senderAcount.publicKey!); - String messageContent; if (computedOptions.messageType == MessageType.REPLY || computedOptions.messageType == MessageType.COMPOSITE) { @@ -86,18 +45,17 @@ Future send(ChatSendOptions options) async { messageContent = computedOptions.messageObj?.content; } - final sendMessagePayload = await getSendMessagePayload( - receiverAddress: computedOptions.to, - senderPublicKey: senderPublicKey, - senderPgpPrivateKey: computedOptions.pgpPrivateKey, - senderAddress: computedOptions.accountAddress, - messageType: computedOptions.messageType, - messageContent: messageContent, - messageObj: computedOptions.messageObj, - group: group, - isValidGroup: isValidGroup); - - log('sendMessagePayload $sendMessagePayload'); + final sendMessagePayload = await sendMessagePayloadCore( + senderConnectedUser: connectedUser!, + receiverAddress: computedOptions.to, + senderPgpPrivateKey: computedOptions.pgpPrivateKey, + senderAddress: computedOptions.account, + messageType: computedOptions.messageType, + messageContent: messageContent, + messageObj: computedOptions.messageObj, + group: group, + isGroup: isGroup, + ); return sendMessageService(sendMessagePayload, isIntent); } catch (e) { @@ -126,112 +84,6 @@ Future sendMessageService( } } -validateSendOptions(ComputedOptions options) async { - if (options.accountAddress == null) { - throw Exception('Account address is required.'); - } - - if (!isValidETHAddress(options.accountAddress!)) { - throw Exception('Invalid address ${options.accountAddress}'); - } - - if (options.pgpPrivateKey == null) { - throw Exception('Private Key is required.'); - } - - if (options.messageType != MessageType.COMPOSITE && - options.messageType != MessageType.REPLY && - options.messageObj?.content.isEmpty) { - throw Exception('Cannot send empty message'); - } - - // TODO: Validate message object -} - -Future getSendMessagePayload({ - required String receiverAddress, - String? senderAddress, - String? senderPgpPrivateKey, - required String senderPublicKey, - required String messageType, - required String messageContent, - dynamic messageObj, - GroupDTO? group, - required bool isValidGroup, -}) async { - final secretKey = generateRandomSecret(15); - - final encryptedMessageContentData = await getEncryptedRequest( - receiverAddress: receiverAddress, - senderPublicKey: senderPublicKey, - senderPgpPrivateKey: senderPgpPrivateKey!, - message: messageContent, - isGroup: isValidGroup, - group: group, - secretKey: secretKey); - final encryptedMessageContent = encryptedMessageContentData.message; - final deprecatedSignature = - removeVersionFromPublicKey(encryptedMessageContentData.signature); - - final encryptedMessageObjData = await getEncryptedRequest( - receiverAddress: receiverAddress, - senderPublicKey: senderPublicKey, - senderPgpPrivateKey: senderPgpPrivateKey, - message: jsonEncode(messageObj?.toJson()), - isGroup: isValidGroup, - group: group, - secretKey: secretKey); - final encryptedMessageObj = encryptedMessageObjData.message; - final encryptionType = encryptedMessageObjData.encryptionType; - final encryptedMessageObjSecret = - removeVersionFromPublicKey(encryptedMessageObjData.aesEncryptedSecret); - - return SendMessagePayload( - fromDID: walletToPCAIP10(senderAddress!), - toDID: !isValidGroup - ? walletToPCAIP10(receiverAddress) - : group?.chatId ?? '', - fromCAIP10: walletToPCAIP10(senderAddress), - toCAIP10: !isValidGroup - ? walletToPCAIP10(receiverAddress) - : group?.chatId ?? '', - messageContent: encryptedMessageContent, - messageObj: encryptionType == 'PlainText' - ? messageObj?.toJson() - : encryptedMessageObj, - messageType: messageType, - signature: deprecatedSignature, - verificationProof: "pgp:$deprecatedSignature", - encType: encryptionType, - encryptedSecret: encryptedMessageObjSecret, - sigType: "pgp"); -} - -class ComputedOptions { - String messageType; - dynamic messageObj; - String to; - String? accountAddress; - String? pgpPrivateKey; - - ComputedOptions( - {required this.messageType, - required this.to, - this.messageObj, - this.accountAddress, - this.pgpPrivateKey}); - - Map toJson() { - return { - 'messageType': messageType, - 'messageObj': messageObj, - 'to': to, - 'accountAddress': accountAddress, - 'pgpPrivateKey': pgpPrivateKey - }; - } -} - class _Content { String content; @@ -244,12 +96,6 @@ class _Content { data['content'] = content; return data; } - - static _Content fromJson(Map json) { - return _Content( - content: json['content'], - ); - } } class _NestedContent { @@ -268,13 +114,6 @@ class _NestedContent { return data; } - static _NestedContent fromJson(Map json) { - return _NestedContent( - messageType: json['messageType'], - messageObj: _Content.fromJson(json['messageObj']), - ); - } - static _NestedContent fromNestedContent(NestedContent nestedContent) { return _NestedContent( messageType: nestedContent.type, @@ -283,7 +122,6 @@ class _NestedContent { } ComputedOptions computeOptions(ChatSendOptions options) { - log('computeOptions method - options $options'); String messageType = options.message?.type ?? options.messageType ?? MessageType.TEXT; dynamic messageObj = options.message; @@ -306,7 +144,7 @@ ComputedOptions computeOptions(ChatSendOptions options) { } } - String to = options.receiverAddress; + String to = options.to; if (to.isEmpty) { throw Exception('Options.to is required'); } @@ -326,7 +164,6 @@ ComputedOptions computeOptions(ChatSendOptions options) { if (messageObj?.compositeContent != null) { messageObj?.compositeContent = messageObj?.compositeContent?.map((nestedContent) { - log("FROM NESTED CONTENT ${_NestedContent.fromNestedContent(nestedContent)}"); return _NestedContent.fromNestedContent(nestedContent); }); } else { @@ -335,9 +172,11 @@ ComputedOptions computeOptions(ChatSendOptions options) { } return ComputedOptions( - messageType: messageType, - messageObj: messageObj, - to: to, - accountAddress: options.accountAddress, - pgpPrivateKey: options.pgpPrivateKey); + messageType: messageType, + messageObj: messageObj, + to: to, + account: options.account, + signer: options.signer, + pgpPrivateKey: options.pgpPrivateKey, + ); } diff --git a/lib/src/chat/src/update_group_members.dart b/lib/src/chat/src/update_group_members.dart index 47463f5..bd5ee0b 100644 --- a/lib/src/chat/src/update_group_members.dart +++ b/lib/src/chat/src/update_group_members.dart @@ -51,12 +51,6 @@ Future updateGroupMembers({ String? encryptedSecret; final group = await getGroupInfo(chatId: chatId); - if (group == null) { - throw Exception('Group not found'); - } - - print('group.isPublic: ${group.isPublic}'); - print('group.encryptedSecret: ${group.encryptedSecret}'); if (!group.isPublic) { if (group.encryptedSecret != null) { diff --git a/lib/src/chat/src/update_group_profile.dart b/lib/src/chat/src/update_group_profile.dart index 3376a98..41328f3 100644 --- a/lib/src/chat/src/update_group_profile.dart +++ b/lib/src/chat/src/update_group_profile.dart @@ -39,9 +39,6 @@ Future updateGroupProfile({ ); final group = await getGroupInfo(chatId: chatId); - if (group == null) { - throw Exception('Group not found'); - } /** * CREATE PROFILE VERIFICATION PROOF */ diff --git a/lib/src/helpers/helpers.dart b/lib/src/helpers/helpers.dart index 41781d3..3b78306 100644 --- a/lib/src/helpers/helpers.dart +++ b/lib/src/helpers/helpers.dart @@ -5,3 +5,4 @@ export 'src/wallet.dart'; export 'src/validators.dart'; export 'src/converters.dart'; export 'src/auth_update_user_service.dart'; +export 'src/get_encrypted_secret.dart'; diff --git a/lib/src/helpers/src/crypto.dart b/lib/src/helpers/src/crypto.dart index d28fce6..a482691 100644 --- a/lib/src/helpers/src/crypto.dart +++ b/lib/src/helpers/src/crypto.dart @@ -237,70 +237,92 @@ List generateRandomBytes(int count) { return List.generate(count, (_) => _rand.nextInt(256)); } +/// Decrypts and verifies a Push Chat Message +/// +/// @param message encrypted chat message +/// +/// @param pgpPublicKey pgp public key of signer of message - used for verification +/// +/// @param pgpPrivateKey pgp private key of receiver - used for decryption Future decryptAndVerifyMessage({ required Message message, - required String privateKeyArmored, + required String pgpPublicKey, + required String pgpPrivateKey, }) async { - // log('decryptAndVerifyMessage - message ${message.toJson()} \n privateKeyArmored $privateKeyArmored'); - - // TODO: Complete message verification - // if (message.verificationProof != null && - // message.verificationProof.split(':')[0] == 'pgpv2') { - // Map bodyToBeHashed = { - // 'fromDID': message.fromDID, - // 'toDID': message.fromDID, - // 'fromCAIP10': message.fromCAIP10, - // 'toCAIP10': message.toCAIP10, - // 'messageObj': message.messageObj, - // 'messageType': message.messageType, - // 'encType': message.encType, - // 'encryptedSecret': message.encryptedSecret, - // }; - // String hash = sha256.convert(utf8.encode(jsonEncode(bodyToBeHashed))).toString(); - // String signature = message.verificationProof.split(':')[1]; - // try { - // await verifySignature( - // messageContent: hash, - // signatureArmored: signature, - // publicKeyArmored: pgpPublicKey, - // ); - // } catch (err) { - // // Handle the verification error as needed. - // } - // } else { - // if (message.link == null) { - // Map bodyToBeHashed = { - // 'fromDID': message.fromDID, - // 'toDID': message.toDID, - // 'messageContent': message.messageContent, - // 'messageType': message.messageType, - // }; - // String hash = sha256.convert(utf8.encode(jsonEncode(bodyToBeHashed))).toString(); - // try { - // await verifySignature( - // messageContent: hash, - // signatureArmored: message.signature, - // publicKeyArmored: pgpPublicKey, - // ); - // } catch (err) { - // await verifySignature( - // messageContent: message.messageContent, - // signatureArmored: message.signature, - // publicKeyArmored: pgpPublicKey, - // ); - // } - // } else { - // try { - // await verifySignature( - // messageContent: message.messageContent, - // signatureArmored: message.signature, - // publicKeyArmored: pgpPublicKey, - // ); - // } catch (err) { - // // Handle the verification error as needed. - // } - // } - // } + /** + * VERIFICATION + * If verification proof is present then check that else check messageContent Signature + */ + + if (message.verificationProof != null && + message.verificationProof!.split(':')[0] == 'pgpv2') { + Map bodyToBeHashed = { + 'fromDID': message.fromDID, + 'toDID': message.fromDID, + 'fromCAIP10': message.fromCAIP10, + 'toCAIP10': message.toCAIP10, + 'messageObj': message.messageObj, + 'messageType': message.messageType, + 'encType': message.encType, + 'encryptedSecret': message.encryptedSecret, + }; + String hash = generateHash(bodyToBeHashed); + String signature = message.verificationProof!.split(':')[1]; + await verifySignature( + messageContent: hash, + signatureArmored: signature, + publicKeyArmored: pgpPublicKey, + ); + } else if (message.verificationProof != null && + message.verificationProof!.split(':')[0] == 'pgpv3') { + final bodyToBeHashed = { + 'fromDID': message.fromDID, + 'toDID': message.fromDID, + 'fromCAIP10': message.fromCAIP10, + 'toCAIP10': message.toCAIP10, + 'messageObj': message.messageObj, + 'messageType': message.messageType, + 'encType': message.encType, + 'sessionKey': message.sessionKey, + 'encryptedSecret': message.encryptedSecret, + }; + String hash = generateHash(bodyToBeHashed); + String signature = message.verificationProof!.split(':')[1]; + await verifySignature( + messageContent: hash, + signatureArmored: signature, + publicKeyArmored: pgpPublicKey, + ); + } else { + if (message.link == null) { + Map bodyToBeHashed = { + 'fromDID': message.fromDID, + 'toDID': message.toDID, + 'messageContent': message.messageContent, + 'messageType': message.messageType, + }; + String hash = generateHash(bodyToBeHashed); + try { + await verifySignature( + messageContent: hash, + signatureArmored: message.signature, + publicKeyArmored: pgpPublicKey, + ); + } catch (e) { + await verifySignature( + messageContent: message.messageContent, + signatureArmored: message.signature, + publicKeyArmored: pgpPublicKey, + ); + } + } else { + await verifySignature( + messageContent: message.messageContent, + signatureArmored: message.signature, + publicKeyArmored: pgpPrivateKey, + ); + } + } /** * DECRYPTION @@ -308,9 +330,18 @@ Future decryptAndVerifyMessage({ * 2. Decrypt messageObj.message, messageObj.meta , messageContent */ try { + /** + * Get encryptedSecret from Backend using sessionKey for this encryption type + */ + if (message.encType == 'pgpv1:group') { + message.encryptedSecret = await getEncryptedSecret( + sessionKey: message.sessionKey!, + ); + } + String secretKey = await pgpDecrypt( cipherText: message.encryptedSecret, - privateKeyArmored: privateKeyArmored, + privateKeyArmored: pgpPrivateKey, ); if (message.messageObj != null) { diff --git a/lib/src/helpers/src/get_encrypted_secret.dart b/lib/src/helpers/src/get_encrypted_secret.dart new file mode 100644 index 0000000..32e2d2b --- /dev/null +++ b/lib/src/helpers/src/get_encrypted_secret.dart @@ -0,0 +1,15 @@ +import 'package:push_restapi_dart/push_restapi_dart.dart'; + +Future getEncryptedSecret({required String sessionKey}) async { + if (sessionKey.isEmpty) { + throw Exception('sessionKey is required'); + } + final result = + await http.get(path: '/v1/chat/encryptedsecret/sessionKey/$sessionKey'); + + if (result == null || result is! Map) { + throw Exception( + result ?? 'Unable to get encrypted secret with sesionkey $sessionKey'); + } + return result['encryptedSecret']; +} diff --git a/lib/src/models/src/message.dart b/lib/src/models/src/message.dart index e51e7b5..14f5ff1 100644 --- a/lib/src/models/src/message.dart +++ b/lib/src/models/src/message.dart @@ -18,6 +18,8 @@ class Message { String encryptedSecret; bool? deprecated; String? deprecatedCode; + String? verificationProof; + String? sessionKey; String get displayText { var content = ''; @@ -64,6 +66,8 @@ class Message { required this.encryptedSecret, this.deprecated, this.deprecatedCode, + this.verificationProof, + this.sessionKey, }); factory Message.fromJson(Map json) { @@ -83,6 +87,8 @@ class Message { encryptedSecret: json['encryptedSecret'], deprecated: json['deprecated'], deprecatedCode: json['deprecatedCode'], + verificationProof: json['verificationProof'], + sessionKey: json['sessionKey'], ); } @@ -103,6 +109,8 @@ class Message { data['encryptedSecret'] = encryptedSecret; data['deprecated'] = deprecated; data['deprecatedCode'] = deprecatedCode; + data['verificationProof'] = verificationProof; + data['sessionKey'] = sessionKey; return data; } } diff --git a/lib/src/spaces/src/helpers/send_live_space_data.dart b/lib/src/spaces/src/helpers/send_live_space_data.dart index 6f0efb3..a43e1da 100644 --- a/lib/src/spaces/src/helpers/send_live_space_data.dart +++ b/lib/src/spaces/src/helpers/send_live_space_data.dart @@ -30,7 +30,10 @@ Future sendLiveSpaceData({ ); final options = ChatSendOptions( - messageType: messageType, message: message, receiverAddress: spaceId); + messageType: messageType, + message: message, + to: spaceId, + ); return send(options); } diff --git a/lib/src/spaces/src/space.dart b/lib/src/spaces/src/space.dart index 5bf40af..f4332fc 100644 --- a/lib/src/spaces/src/space.dart +++ b/lib/src/spaces/src/space.dart @@ -322,7 +322,7 @@ class PushSpaceNotifier extends ChangeNotifier { final options = ChatSendOptions( message: reactionMessage, - receiverAddress: data.spaceId, + to: data.spaceId, ); await send(options); diff --git a/lib/src/user/src/create_user.dart b/lib/src/user/src/create_user.dart index cb7a02a..980c16a 100644 --- a/lib/src/user/src/create_user.dart +++ b/lib/src/user/src/create_user.dart @@ -71,10 +71,8 @@ Future createUserEmpty({required String accountAddress}) async { final result = await http.post(path: '/v2/users', data: data); - if (result == null) { - return null; - } else if (result is String) { - throw Exception(result); + if (result == null || result is String) { + throw Exception(result ?? 'Unable to create account for $accountAddress'); } else { return User.fromJson(result); } From e65148bdd601e12531c870155a5e4b81dd637f6e Mon Sep 17 00:00:00 2001 From: gbogboadePush <135420608+gbogboadePush@users.noreply.github.com> Date: Thu, 14 Dec 2023 10:35:17 +0100 Subject: [PATCH 06/14] Remove the user property from the ConnectedUser class (#75) * feat: add scalability changes fo updarte group * chore: add fromUser constructor for ConnectedUser --------- Co-authored-by: Gbogboade Ayomide Co-authored-by: Madhur Gupta --- .../payload/send_notification.dart | 4 +- .../test_functions/user/test_create_user.dart | 2 +- .../group/chat_room/chat_room_provider.dart | 6 +- lib/src/chat/src/approve_request.dart | 4 +- lib/src/chat/src/create_group.dart | 2 +- lib/src/chat/src/get_group.dart | 2 +- lib/src/chat/src/helper/user.dart | 34 +++-- lib/src/chat/src/update_group.dart | 127 ++++++++++++++---- lib/src/chat/src/update_group_config.dart | 2 +- lib/src/chat/src/update_group_members.dart | 10 +- lib/src/chat/src/update_group_profile.dart | 3 +- lib/src/models/src/user_model.dart | 28 ++-- lib/src/spaces/src/get_space_by_id.dart | 8 +- lib/src/spaces/src/info.dart | 9 +- lib/src/user/src/create_user.dart | 8 +- lib/src/user/src/get_user.dart | 7 +- 16 files changed, 168 insertions(+), 88 deletions(-) diff --git a/example/lib/test_functions/payload/send_notification.dart b/example/lib/test_functions/payload/send_notification.dart index e1ea26d..b925c54 100644 --- a/example/lib/test_functions/payload/send_notification.dart +++ b/example/lib/test_functions/payload/send_notification.dart @@ -38,7 +38,7 @@ void testSendVideoCallNotification() async { // 5. Decrypt keys in Grp final pgpPrivateKey = await push.decryptPGPKey( - encryptedPGPPrivateKey: user?.encryptedPrivateKey as String, + encryptedPGPPrivateKey: user.encryptedPrivateKey as String, wallet: push.getWallet(signer: signer), ); @@ -47,7 +47,7 @@ void testSendVideoCallNotification() async { senderType: 1, signer: signer, pgpPrivateKey: pgpPrivateKey, - pgpPublicKey: user?.publicKey, + pgpPublicKey: user.publicKey, chatId: group?.chatId, type: push.NOTIFICATION_TYPE.BROADCAST, // broadcast identityType: push.IDENTITY_TYPE.DIRECT_PAYLOAD, // direct payload diff --git a/example/lib/test_functions/user/test_create_user.dart b/example/lib/test_functions/user/test_create_user.dart index 86577a5..b9d9ab6 100644 --- a/example/lib/test_functions/user/test_create_user.dart +++ b/example/lib/test_functions/user/test_create_user.dart @@ -44,7 +44,7 @@ void testCreateRandomUser() async { }, ); - print(result?.did); + print(result.did); } catch (e) { print(e); } diff --git a/example/lib/views/group/chat_room/chat_room_provider.dart b/example/lib/views/group/chat_room/chat_room_provider.dart index 8af1b08..0f13564 100644 --- a/example/lib/views/group/chat_room/chat_room_provider.dart +++ b/example/lib/views/group/chat_room/chat_room_provider.dart @@ -208,10 +208,8 @@ class ChatRoomProvider extends ChangeNotifier { Future getLatesGroupInfo() async { final result = await getGroup(chatId: _currentChatid); - if (result != null) { - _room.groupInformation = result; - notifyListeners(); - } + _room.groupInformation = result; + notifyListeners(); } GroupDTO? get groupInformation => _room.groupInformation; diff --git a/lib/src/chat/src/approve_request.dart b/lib/src/chat/src/approve_request.dart index 9270fa5..03548d5 100644 --- a/lib/src/chat/src/approve_request.dart +++ b/lib/src/chat/src/approve_request.dart @@ -64,7 +64,7 @@ Future approve({ ); final publickKeys = groupMembers.map((e) => e.publicKey).toList(); - publickKeys.add(connectedUser!.publicKey!); + publickKeys.add(connectedUser.publicKey!); encryptedSecret = await pgpEncrypt(plainText: secretKey, keys: publickKeys); } @@ -81,7 +81,7 @@ Future approve({ final signature = await sign( message: hash, - privateKey: connectedUser!.privateKey!, + privateKey: connectedUser.privateKey!, ); final body = { diff --git a/lib/src/chat/src/create_group.dart b/lib/src/chat/src/create_group.dart index e41677e..e2ff6ac 100644 --- a/lib/src/chat/src/create_group.dart +++ b/lib/src/chat/src/create_group.dart @@ -62,7 +62,7 @@ Future createGroup({ final hash = generateHash(bodyToBeHashed); final signature = await sign( message: hash, - privateKey: connectedUser!.privateKey!, + privateKey: connectedUser.privateKey!, ); const sigType = 'pgp'; diff --git a/lib/src/chat/src/get_group.dart b/lib/src/chat/src/get_group.dart index ff66a1e..8aeb340 100644 --- a/lib/src/chat/src/get_group.dart +++ b/lib/src/chat/src/get_group.dart @@ -1,6 +1,6 @@ import 'package:push_restapi_dart/push_restapi_dart.dart'; -Future getGroup({required String chatId}) async { +Future getGroup({required String chatId}) async { if (chatId.isEmpty) { throw Exception('chatId cannot be null or empty'); } diff --git a/lib/src/chat/src/helper/user.dart b/lib/src/chat/src/helper/user.dart index ca1483b..f530bb0 100644 --- a/lib/src/chat/src/helper/user.dart +++ b/lib/src/chat/src/helper/user.dart @@ -1,24 +1,40 @@ import 'package:push_restapi_dart/push_restapi_dart.dart'; -Future getConnectedUserV2({ +Future getConnectedUserV2({ + required Wallet wallet, + String? privateKey, +}) async { + return getConnectedUserV2Core(wallet: wallet, privateKey: privateKey); +} + +Future getConnectedUserV2Core({ required Wallet wallet, String? privateKey, }) async { final user = await getUser(address: getAccountAddress(wallet)); - if (user == null) { - return null; - } + if (user != null && user.encryptedPrivateKey != null) { + if (privateKey != null) { + return ConnectedUser.fromUser(user: user, privateKey: privateKey); + } else { + final decryptedPrivateKey = await getDecryptedPrivateKey( + address: wallet.address!, + wallet: wallet, + user: user, + ); - if (privateKey != null) { - return ConnectedUser(user: user, privateKey: privateKey); + return ConnectedUser.fromUser( + user: user, privateKey: decryptedPrivateKey); + } } else { + final newUser = + await createUser(signer: wallet.signer!, progressHook: (hook) {}); final decryptedPrivateKey = await getDecryptedPrivateKey( address: wallet.address!, wallet: wallet, - user: user, + user: newUser, ); - - return ConnectedUser(user: user, privateKey: decryptedPrivateKey); + return ConnectedUser.fromUser( + user: newUser, privateKey: decryptedPrivateKey); } } diff --git a/lib/src/chat/src/update_group.dart b/lib/src/chat/src/update_group.dart index e34e455..6657332 100644 --- a/lib/src/chat/src/update_group.dart +++ b/lib/src/chat/src/update_group.dart @@ -1,24 +1,58 @@ import '../../../push_restapi_dart.dart'; -Future updateGroup( - {required String chatId, - String? account, - Signer? signer, - required String groupName, - required String groupDescription, - String? groupImage, - required List members, - required List admins, - required bool isPublic, - String? contractAddressNFT, - int? numberOfNFTs, - String? contractAddressERC20, - int? numberOfERC20, - String? pgpPrivateKey, - String? meta, - DateTime? scheduleAt, - DateTime? scheduleEnd, - ChatStatus? status}) async { +Future updateGroup({ + required String chatId, + String? account, + Signer? signer, + required String groupName, + required String groupDescription, + String? groupImage, + required List members, + required List admins, + required bool isPublic, + String? pgpPrivateKey, + String? meta, + DateTime? scheduleAt, + DateTime? scheduleEnd, + ChatStatus? status, + Map? rules, +}) async { + return updateGroupCore( + chatId: chatId, + groupName: groupName, + groupDescription: groupDescription, + members: members, + admins: admins, + isPublic: isPublic, + account: account, + groupImage: groupImage, + meta: meta, + pgpPrivateKey: pgpPrivateKey, + rules: rules, + scheduleAt: scheduleAt, + scheduleEnd: scheduleEnd, + signer: signer, + status: status, + ); +} + +Future updateGroupCore({ + required String chatId, + String? account, + Signer? signer, + required String groupName, + required String groupDescription, + String? groupImage, + required List members, + required List admins, + required bool isPublic, + String? pgpPrivateKey, + String? meta, + DateTime? scheduleAt, + DateTime? scheduleEnd, + ChatStatus? status, + Map? rules, +}) async { try { account ??= getCachedWallet()?.address; signer ??= getCachedWallet()?.signer; @@ -46,17 +80,56 @@ Future updateGroup( privateKey: pgpPrivateKey, ); - final convertedMembersDIDList = + final convertedMembers = await Future.wait(members.map((item) => getUserDID(address: item))); - final convertedAdminsDIDList = + final convertedAdmins = await Future.wait(admins.map((item) => getUserDID(address: item))); + final groupChat = await getGroup(chatId: chatId); + + // Compare members array with updateGroup.members array. If they have all the same elements then return true + final updatedParticipants = + Set.from(convertedAdmins.map((e) => e.toLowerCase())); + + final participantStatus = + await getGroupMemberStatus(chatId: chatId, did: connectedUser.did!); + + var sameMembers = true; + + for (var member in groupChat.members) { + if (!updatedParticipants.contains(member.wallet.toLowerCase())) { + sameMembers = false; + } + } + + String? encryptedSecret; + if ((!sameMembers || !participantStatus.isMember) && !groupChat.isPublic) { + final secretKey = generateRandomSecret(15); + var publicKeys = []; + + // This will now only take keys of non-removed members + for (var member in groupChat.members) { + if (updatedParticipants.contains(member.wallet.toLowerCase())) { + publicKeys.add(member.publicKey!); + } + } + + // This is autoJoin Case + if (!participantStatus.isMember) { + publicKeys.add(connectedUser.publicKey!); + } + + // Encrypt secret key with group members public keys + encryptedSecret = + await pgpEncrypt(plainText: secretKey, keys: publicKeys); + } + final bodyToBeHashed = { 'groupName': groupName, 'groupDescription': groupDescription, 'groupImage': groupImage, - 'members': convertedMembersDIDList, - 'admins': convertedAdminsDIDList, + 'members': convertedMembers, + 'admins': convertedAdmins, 'chatId': chatId, }; @@ -64,7 +137,7 @@ Future updateGroup( final signature = await sign( message: hash, - privateKey: connectedUser!.privateKey!, + privateKey: connectedUser.privateKey!, ); final sigType = 'pgp'; @@ -74,14 +147,16 @@ Future updateGroup( 'groupName': groupName, 'groupImage': groupImage, 'groupDescription': groupDescription, - 'members': convertedMembersDIDList, - 'admins': convertedAdminsDIDList, + 'members': convertedMembers, + 'admins': convertedAdmins, 'address': 'eip155:$userDID', 'verificationProof': verificationProof, + 'encryptedSecret': encryptedSecret, 'scheduleAt': scheduleAt?.toIso8601String(), 'scheduleEnd': scheduleEnd?.toIso8601String(), if (meta != null) 'meta': meta, if (status != null) 'status': chatStringFromChatStatus(status), + if (rules != null) 'rules': rules, }; final result = await http.put( diff --git a/lib/src/chat/src/update_group_config.dart b/lib/src/chat/src/update_group_config.dart index 7976f95..858584f 100644 --- a/lib/src/chat/src/update_group_config.dart +++ b/lib/src/chat/src/update_group_config.dart @@ -40,7 +40,7 @@ Future updateGroupConfig({ final signature = await sign( message: hash, - privateKey: connectedUser!.privateKey!, + privateKey: connectedUser.privateKey!, ); final sigType = 'pgpv2'; diff --git a/lib/src/chat/src/update_group_members.dart b/lib/src/chat/src/update_group_members.dart index bd5ee0b..ceb79c1 100644 --- a/lib/src/chat/src/update_group_members.dart +++ b/lib/src/chat/src/update_group_members.dart @@ -54,9 +54,9 @@ Future updateGroupMembers({ if (!group.isPublic) { if (group.encryptedSecret != null) { - final isMember = (await getGroupMemberStatus( - chatId: chatId, did: connectedUser!.user.did!)) - .isMember; + final isMember = + (await getGroupMemberStatus(chatId: chatId, did: connectedUser.did!)) + .isMember; var groupMembers = await getAllGroupMembersPublicKeys(chatId: chatId); @@ -85,7 +85,7 @@ Future updateGroupMembers({ // This is autoJoin Case if (!isMember) { - publicKeys.add(connectedUser.user.publicKey!); + publicKeys.add(connectedUser.publicKey!); } encryptedSecret = await pgpEncrypt( @@ -106,7 +106,7 @@ Future updateGroupMembers({ final signature = await sign( message: hash, - privateKey: connectedUser!.privateKey!, + privateKey: connectedUser.privateKey!, ); final sigType = 'pgpv2'; diff --git a/lib/src/chat/src/update_group_profile.dart b/lib/src/chat/src/update_group_profile.dart index 41328f3..9d0c79d 100644 --- a/lib/src/chat/src/update_group_profile.dart +++ b/lib/src/chat/src/update_group_profile.dart @@ -39,6 +39,7 @@ Future updateGroupProfile({ ); final group = await getGroupInfo(chatId: chatId); + /** * CREATE PROFILE VERIFICATION PROOF */ @@ -54,7 +55,7 @@ Future updateGroupProfile({ final signature = await sign( message: hash, - privateKey: connectedUser!.privateKey!, + privateKey: connectedUser.privateKey!, ); final sigType = 'pgpv2'; diff --git a/lib/src/models/src/user_model.dart b/lib/src/models/src/user_model.dart index f570bcb..09fe35c 100644 --- a/lib/src/models/src/user_model.dart +++ b/lib/src/models/src/user_model.dart @@ -28,23 +28,25 @@ abstract class Signer { } class ConnectedUser extends User { - final User user; final String? privateKey; ConnectedUser({ - required this.user, required this.privateKey, - }) { - super.did = user.did; - super.profile = user.profile; - super.name = user.name; - super.about = user.about; - super.verificationProof = user.verificationProof; - super.publicKey = user.publicKey; - super.msgSent = user.msgSent; - super.maxMsgPersisted = user.maxMsgPersisted; - super.wallets = user.wallets; - super.encryptedPrivateKey = user.encryptedPrivateKey; + }); + + static ConnectedUser fromUser({required User user, String? privateKey}) { + var connectedUser = ConnectedUser(privateKey: privateKey); + connectedUser.did = user.did; + connectedUser.profile = user.profile; + connectedUser.name = user.name; + connectedUser.about = user.about; + connectedUser.verificationProof = user.verificationProof; + connectedUser.publicKey = user.publicKey; + connectedUser.msgSent = user.msgSent; + connectedUser.maxMsgPersisted = user.maxMsgPersisted; + connectedUser.wallets = user.wallets; + connectedUser.encryptedPrivateKey = user.encryptedPrivateKey; + return connectedUser; } } diff --git a/lib/src/spaces/src/get_space_by_id.dart b/lib/src/spaces/src/get_space_by_id.dart index ab35f5d..d0bcd4a 100644 --- a/lib/src/spaces/src/get_space_by_id.dart +++ b/lib/src/spaces/src/get_space_by_id.dart @@ -6,13 +6,9 @@ Future getSpaceById({ }) async { try { final group = await push.getGroup(chatId: spaceId); - if (group != null) { - return groupDtoToSpaceDto(group); - } else { - throw Exception('Space not found for id : $spaceId'); - } + return groupDtoToSpaceDto(group); } catch (e) { print('[Push SDK] - API - Error - API update -: $e'); - rethrow; + throw Exception('Space not found for id : $spaceId'); } } diff --git a/lib/src/spaces/src/info.dart b/lib/src/spaces/src/info.dart index c30f092..e5d273b 100644 --- a/lib/src/spaces/src/info.dart +++ b/lib/src/spaces/src/info.dart @@ -8,13 +8,10 @@ Future info({ throw Exception("spaceId cannot be null or empty"); } final group = await getGroup(chatId: spaceId); - if (group != null) { - return groupDtoToSpaceDto(group); - } else { - throw Exception('Space not found for id : $spaceId'); - } + return groupDtoToSpaceDto(group); } catch (e) { print('[Push SDK] - API - Error - API update -: $e'); - rethrow; + + throw Exception('Space not found for id : $spaceId'); } } diff --git a/lib/src/user/src/create_user.dart b/lib/src/user/src/create_user.dart index 980c16a..fd846e8 100644 --- a/lib/src/user/src/create_user.dart +++ b/lib/src/user/src/create_user.dart @@ -2,7 +2,7 @@ import 'dart:convert'; import '../../../push_restapi_dart.dart'; -Future createUser({ +Future createUser({ required Signer signer, String version = ENCRYPTION_TYPE.PGP_V3, required Function(ProgressHookType) progressHook, @@ -48,10 +48,8 @@ Future createUser({ final result = await http.post(path: '/v2/users', data: data); - if (result == null) { - return null; - } else if (result is String) { - throw Exception(result); + if (result == null || result is String) { + throw Exception(result ?? 'Unable to create account for $address'); } else { return User.fromJson(result); } diff --git a/lib/src/user/src/get_user.dart b/lib/src/user/src/get_user.dart index 8e17f9d..3ba22c7 100644 --- a/lib/src/user/src/get_user.dart +++ b/lib/src/user/src/get_user.dart @@ -10,12 +10,9 @@ Future getUser({ final caip10 = walletToPCAIP10(address); final requestUrl = '/v2/users/?caip10=$caip10'; final result = await http.get(path: requestUrl); - if (result == null || result.isEmpty) { - return null; - } - if (result is String) { - throw Exception("Unable to get user"); + if (result == null || result is String) { + throw Exception(result ?? "Unable to get user with address $address"); } return User.fromJson(result); From ed86d3372e2b71435467d5b913f89a7111b019df Mon Sep 17 00:00:00 2001 From: Gbogboade Ayomide Date: Thu, 14 Dec 2023 11:29:11 +0100 Subject: [PATCH 07/14] feat: add update group info screen --- example/lib/views/group/__group.dart | 1 + .../group/chat_room/chat_room_provider.dart | 1 - .../views/group/edit_group_info.screen.dart | 126 ++++++++++++++++++ .../lib/views/group/group_members_dialog.dart | 19 +++ example/pubspec.yaml | 4 +- 5 files changed, 147 insertions(+), 4 deletions(-) create mode 100644 example/lib/views/group/edit_group_info.screen.dart diff --git a/example/lib/views/group/__group.dart b/example/lib/views/group/__group.dart index 24bed4d..f710ed9 100644 --- a/example/lib/views/group/__group.dart +++ b/example/lib/views/group/__group.dart @@ -10,3 +10,4 @@ export 'requests/request_provider.dart'; export 'create_group_screen.dart'; export 'group_members_dialog.dart'; export 'add_group_member.dart'; +export 'edit_group_info.screen.dart'; \ No newline at end of file diff --git a/example/lib/views/group/chat_room/chat_room_provider.dart b/example/lib/views/group/chat_room/chat_room_provider.dart index b5a2ee5..bb5d486 100644 --- a/example/lib/views/group/chat_room/chat_room_provider.dart +++ b/example/lib/views/group/chat_room/chat_room_provider.dart @@ -196,7 +196,6 @@ class ChatRoomProvider extends ChangeNotifier { updateSending(true); final message = await send(options); updateSending(false); - print('onSendMessage...5..after send..${message?.toJson()}'); if (message != null) { getRoomMessages(); } diff --git a/example/lib/views/group/edit_group_info.screen.dart b/example/lib/views/group/edit_group_info.screen.dart new file mode 100644 index 0000000..ce2926d --- /dev/null +++ b/example/lib/views/group/edit_group_info.screen.dart @@ -0,0 +1,126 @@ +import 'package:push_restapi_dart/push_restapi_dart.dart' as push; +import 'dart:convert'; +import 'dart:io'; + +import '../../../__lib.dart'; +import 'package:push_restapi_dart/push_restapi_dart.dart'; + +class EditGroupInfoScreen extends ConsumerStatefulWidget { + const EditGroupInfoScreen({super.key}); + + @override + ConsumerState createState() => + _EditGroupInfoScreenState(); +} + +class _EditGroupInfoScreenState extends ConsumerState { + TextEditingController nameController = TextEditingController(); + TextEditingController descriptionController = TextEditingController(); + Feeds? room; + @override + void initState() { + super.initState(); + + WidgetsBinding.instance.addPostFrameCallback((_) { + setState(() { + room = ref.read(chatRoomProvider).room; + nameController.text = room!.groupInformation!.groupName!; + descriptionController.text = room!.groupInformation!.groupDescription!; + }); + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('Update Group Info'), + ), + body: ListView( + padding: EdgeInsets.symmetric(horizontal: 16), + children: [ + SizedBox(height: 64), + InkWell( + onTap: () { + onSelectFile(); + }, + child: (selectedFile != null) + ? Container( + height: 100, + width: 100, + decoration: BoxDecoration( + shape: BoxShape.circle, + image: + DecorationImage(image: FileImage(selectedFile!))), + ) + : ProfileImage( + imageUrl: room?.groupInformation?.groupImage, + size: 100, + ), + ), + SizedBox(height: 16), + InputField( + controller: nameController, + label: 'Group Name', + ), + SizedBox(height: 16), + InputField( + controller: descriptionController, + label: 'Group Description', + ), + SizedBox(height: 64), + MaterialButton( + color: pushColor, + shape: + RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + onPressed: onSubmit, + textColor: Colors.white, + child: Text('Save'), + padding: EdgeInsets.all(16), + ), + ], + ), + ); + } + + File? _selectedFile; + File? get selectedFile => _selectedFile; + Future onSelectFile() async { + Get.bottomSheet(AttachmentDialog( + onSelect: (File? file, String type) async { + pop(); + if (file != null) { + setState(() { + _selectedFile = file; + }); + } + }, + )); + } + + String composeImage() { + final img = base64Encode(selectedFile!.readAsBytesSync()); + return jsonEncode({'content': img}); + } + + onSubmit() async { + try { + showLoadingDialog(); + await push.updateGroupProfile( + chatId: room!.chatId!, + groupName: nameController.text.trim(), + groupImage: selectedFile != null + ? composeImage() + : room!.groupInformation!.groupImage!, + groupDescription: descriptionController.text.trim(), + ); + + await ref.read(chatRoomProvider).getLatesGroupInfo(); + pop(); + pop(); + + } catch (e) { + pop(); + } + } +} diff --git a/example/lib/views/group/group_members_dialog.dart b/example/lib/views/group/group_members_dialog.dart index d99b1ec..32ae518 100644 --- a/example/lib/views/group/group_members_dialog.dart +++ b/example/lib/views/group/group_members_dialog.dart @@ -36,6 +36,25 @@ class GroupMembersDialog extends ConsumerWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + Align( + alignment: Alignment.centerRight, + child: MaterialButton( + onPressed: () { + pop(); + pushScreen(EditGroupInfoScreen()); + }, + color: pushColor, + textColor: Colors.white, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.settings), + SizedBox(width: 4), + KText('Edit Group Info'), + ], + ), + ), + ), TabBar(tabs: [ Tab(text: 'Admin'), Tab(text: 'Members'), diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 015338d..ea5a534 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -2,13 +2,11 @@ name: example description: A starting point for Dart libraries or applications. version: 1.0.0 publish_to: none -# repository: https://github.com/my_org/my_repo environment: sdk: '>=2.19.1 <3.0.0' -# dependencies: -# path: ^1.8.0 + dev_dependencies: lints: ^2.0.0 From f43adb9dc2081ee8cbac186663abf4c495b750a1 Mon Sep 17 00:00:00 2001 From: Gbogboade Ayomide Date: Fri, 15 Dec 2023 11:03:22 +0100 Subject: [PATCH 08/14] feat: implement group scalability changes for group in example app --- example/lib/views/group/add_group_member.dart | 2 +- .../group/chat_room/chat_room_provider.dart | 67 ++++++++++++------- .../group/chat_room/chat_room_screen.dart | 2 +- .../views/group/edit_group_info.screen.dart | 22 +++--- .../lib/views/group/group_members_dialog.dart | 52 +++++++------- example/lib/views/home_screen.dart | 23 +++++++ lib/src/chat/src/models/chat_members.dart | 22 ++++-- lib/src/chat/src/models/group_info_dto.dart | 4 ++ lib/src/models/src/all.dart | 16 +++-- 9 files changed, 140 insertions(+), 70 deletions(-) diff --git a/example/lib/views/group/add_group_member.dart b/example/lib/views/group/add_group_member.dart index 2a58941..b4709e3 100644 --- a/example/lib/views/group/add_group_member.dart +++ b/example/lib/views/group/add_group_member.dart @@ -85,7 +85,7 @@ class _AddGroupMemberState extends ConsumerState { message: 'Failed to add $address to group', ); } else { - ref.read(chatRoomProvider).onRefreshRoom(groupData: result); + ref.read(chatRoomProvider).getLatestGroupMembers(); controller.clear(); setState(() {}); showMyDialog( diff --git a/example/lib/views/group/chat_room/chat_room_provider.dart b/example/lib/views/group/chat_room/chat_room_provider.dart index bb5d486..4723f4f 100644 --- a/example/lib/views/group/chat_room/chat_room_provider.dart +++ b/example/lib/views/group/chat_room/chat_room_provider.dart @@ -39,15 +39,32 @@ class ChatRoomProvider extends ChangeNotifier { final chatId = room.chatId!; _room = room; _messageList = _localMessagesCache[chatId] ?? []; + _groupInfoDTO = room.groupInformation != null + ? GroupInfoDTO.fromGroupDTO(room.groupInformation!) + : null; _currentChatid = chatId; controller.clear(); - notifyListeners(); getRoomMessages(); - if (room.groupInformation != null) { - getLatesGroupInfo(); + if (_groupInfoDTO != null) { + getLatestGroupInfo(); + _members = (room.groupInformation?.members ?? []) + .map( + (e) => ChatMemberProfile( + address: e.wallet, + intent: false, + role: e.isAdmin ? GroupMemberRole.admin : GroupMemberRole.member, + userInfo: null, + ), + ) + .toList(); + + getLatestGroupMembers(); + } else { + _members.clear(); } + notifyListeners(); } Message? replyTo; @@ -56,18 +73,17 @@ class ChatRoomProvider extends ChangeNotifier { notifyListeners(); } - onRefreshRoom({ + Future onRefreshRoom({ GroupInfoDTO? groupData, }) async { if (groupData != null && groupData.chatId == _currentChatid) { - _room.groupInformation = - GroupDTO.fromJson(groupData.toJson()); //groupData; + _room.groupInformation = GroupDTO.fromJson(groupData.toJson()); notifyListeners(); } getRoomMessages(); - getLatesGroupInfo(); + getLatestGroupInfo(); } Future getRoomMessages() async { @@ -205,33 +221,38 @@ class ChatRoomProvider extends ChangeNotifier { } } - Future getLatesGroupInfo() async { - final result = await getGroup(chatId: _currentChatid); - _room.groupInformation = result; + GroupInfoDTO? _groupInfoDTO; + + Future getLatestGroupInfo() async { + _groupInfoDTO = await getGroupInfo(chatId: _currentChatid); + notifyListeners(); + } + + Future getLatestGroupMembers() async { + _members = await getGroupMembers(chatId: _currentChatid); notifyListeners(); } - GroupDTO? get groupInformation => _room.groupInformation; + GroupInfoDTO? get groupInformation => _groupInfoDTO; - List get admins { - return groupInformation?.members - .where((element) => element.isAdmin == true) - .toList() ?? - []; + List _members = []; + List get admins { + return _members + .where((element) => element.role == GroupMemberRole.admin) + .toList(); } - List get members => - groupInformation?.members - .where((element) => element.isAdmin != true) - .toList() ?? - []; + List get members => _members + .where((element) => element.role == GroupMemberRole.member) + .toList(); - List get pendingMembers => groupInformation?.pendingMembers ?? []; + List get pendingMembers => + _members.where((element) => !element.intent).toList(); String get currentUser => ref.read(accountProvider).pushWallet?.address ?? ''; bool get isUserAdmin => - admins.map((e) => e.wallet).contains(walletToPCAIP10(currentUser)); + admins.map((e) => e.address).contains(walletToPCAIP10(currentUser)); File? _selectedFile; File? get selectedFile => _selectedFile; diff --git a/example/lib/views/group/chat_room/chat_room_screen.dart b/example/lib/views/group/chat_room/chat_room_screen.dart index 36ba3fb..591a182 100644 --- a/example/lib/views/group/chat_room/chat_room_screen.dart +++ b/example/lib/views/group/chat_room/chat_room_screen.dart @@ -70,7 +70,7 @@ class _ChatRoomScreenState extends ConsumerState { ), ) ], - title: Text('${room.groupInformation?.groupName ?? room.intentSentBy}'), + title: Text('${roomVm.groupInformation?.groupName ?? room.intentSentBy}'), ), body: GestureDetector( onTap: () { diff --git a/example/lib/views/group/edit_group_info.screen.dart b/example/lib/views/group/edit_group_info.screen.dart index ce2926d..0638503 100644 --- a/example/lib/views/group/edit_group_info.screen.dart +++ b/example/lib/views/group/edit_group_info.screen.dart @@ -16,16 +16,16 @@ class EditGroupInfoScreen extends ConsumerStatefulWidget { class _EditGroupInfoScreenState extends ConsumerState { TextEditingController nameController = TextEditingController(); TextEditingController descriptionController = TextEditingController(); - Feeds? room; + GroupInfoDTO? groupInfo; @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { setState(() { - room = ref.read(chatRoomProvider).room; - nameController.text = room!.groupInformation!.groupName!; - descriptionController.text = room!.groupInformation!.groupDescription!; + groupInfo = ref.read(chatRoomProvider).groupInformation; + nameController.text = groupInfo!.groupName; + descriptionController.text = groupInfo!.groupDescription; }); }); } @@ -54,7 +54,7 @@ class _EditGroupInfoScreenState extends ConsumerState { DecorationImage(image: FileImage(selectedFile!))), ) : ProfileImage( - imageUrl: room?.groupInformation?.groupImage, + imageUrl: groupInfo?.groupImage, size: 100, ), ), @@ -100,25 +100,23 @@ class _EditGroupInfoScreenState extends ConsumerState { String composeImage() { final img = base64Encode(selectedFile!.readAsBytesSync()); - return jsonEncode({'content': img}); + return 'data:image/png;base64,$img'; } onSubmit() async { try { showLoadingDialog(); await push.updateGroupProfile( - chatId: room!.chatId!, + chatId: groupInfo!.chatId, groupName: nameController.text.trim(), - groupImage: selectedFile != null - ? composeImage() - : room!.groupInformation!.groupImage!, + groupImage: + selectedFile != null ? composeImage() : groupInfo!.groupImage!, groupDescription: descriptionController.text.trim(), ); - await ref.read(chatRoomProvider).getLatesGroupInfo(); + await ref.read(chatRoomProvider).getLatestGroupInfo(); pop(); pop(); - } catch (e) { pop(); } diff --git a/example/lib/views/group/group_members_dialog.dart b/example/lib/views/group/group_members_dialog.dart index 32ae518..966ef53 100644 --- a/example/lib/views/group/group_members_dialog.dart +++ b/example/lib/views/group/group_members_dialog.dart @@ -86,7 +86,7 @@ class GroupAdminsView extends StatelessWidget { required this.chatId, }); - final List admins; + final List admins; final bool isUserAdmin; final String? chatId; @@ -114,9 +114,10 @@ class GroupAdminsView extends StatelessWidget { child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - ProfileImage( - imageUrl: item.image, + BlockiesAvatar( + address: item.address, size: 48, + radius: 10, ), SizedBox(width: 16), Expanded( @@ -124,7 +125,7 @@ class GroupAdminsView extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ - Text('${item.wallet}'), + Text('${item.address}'), Align( alignment: Alignment.topRight, child: MemberActionWidget( @@ -172,7 +173,7 @@ class GroupMembersView extends StatelessWidget { required this.chatId, }); - final List members; + final List members; final bool isUserAdmin; final String? chatId; @@ -201,9 +202,10 @@ class GroupMembersView extends StatelessWidget { child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - ProfileImage( - imageUrl: item.image, + BlockiesAvatar( + address: item.address, size: 48, + radius: 10, ), SizedBox(width: 16), Expanded( @@ -211,7 +213,7 @@ class GroupMembersView extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ - Text('${item.wallet}'), + Text('${item.address}'), MemberActionWidget( item: item, isRemoveAdmin: false, @@ -258,7 +260,7 @@ class MemberActionWidget extends ConsumerStatefulWidget { required this.chatId, }); - final MemberDTO item; + final ChatMemberProfile item; final bool isUserAdmin; final bool isRemoveAdmin; final String? chatId; @@ -271,7 +273,7 @@ class _MemberActionWidgetState extends ConsumerState { @override Widget build(BuildContext context) { final currentUser = ref.read(accountProvider).pushWallet?.address; - if (widget.item.wallet == walletToPCAIP10(currentUser!)) { + if (widget.item.address == walletToPCAIP10(currentUser!)) { return Container( padding: EdgeInsets.all(6), decoration: BoxDecoration( @@ -325,11 +327,11 @@ class _MemberActionWidgetState extends ConsumerState { showLoadingDialog(); removeMembers( chatId: widget.chatId!, - members: [widget.item.wallet], + members: [widget.item.address], ).then((value) { pop(); - ref.read(chatRoomProvider).onRefreshRoom(groupData: value); + ref.read(chatRoomProvider).getLatestGroupMembers(); showMyDialog( context: context, title: 'Remove User', @@ -344,11 +346,11 @@ class _MemberActionWidgetState extends ConsumerState { showLoadingDialog(); removeAdmins( chatId: widget.chatId!, - admins: [widget.item.wallet], + admins: [widget.item.address], ).then((value) { pop(); - ref.read(chatRoomProvider).onRefreshRoom(groupData: value); + ref.read(chatRoomProvider).getLatestGroupMembers(); showMyDialog( context: context, title: 'Remove User', @@ -363,15 +365,15 @@ class _MemberActionWidgetState extends ConsumerState { showLoadingDialog(); removeAdmins( chatId: widget.chatId!, - admins: [widget.item.wallet], + admins: [widget.item.address], ).then((value) { addMembers( chatId: widget.chatId!, - members: [widget.item.wallet], + members: [widget.item.address], ).then((value) { pop(); - ref.read(chatRoomProvider).onRefreshRoom(groupData: value); + ref.read(chatRoomProvider).getLatestGroupMembers(); showMyDialog( context: context, title: 'Demote to member', @@ -387,11 +389,11 @@ class _MemberActionWidgetState extends ConsumerState { showLoadingDialog(); removeMembers( chatId: widget.chatId!, - members: [widget.item.wallet], + members: [widget.item.address], ).then((value) { - addAdmins(chatId: widget.chatId!, admins: [widget.item.wallet]) + addAdmins(chatId: widget.chatId!, admins: [widget.item.address]) .then((value) { - ref.read(chatRoomProvider).onRefreshRoom(groupData: value); + ref.read(chatRoomProvider).getLatestGroupMembers(); pop(); showMyDialog( context: context, @@ -413,7 +415,7 @@ class GroupPendingMembersView extends StatelessWidget { required this.chatId, }); - final List pendingMembers; + final List pendingMembers; final bool isUserAdmin; final String? chatId; @@ -430,8 +432,12 @@ class GroupPendingMembersView extends StatelessWidget { itemBuilder: (context, index) { final item = pendingMembers[index]; return ListTile( - leading: ProfileImage(imageUrl: item.image), - title: Text('${item.wallet}'), + leading: BlockiesAvatar( + address: item.address, + size: 48, + radius: 10, + ), + title: Text('${item.address}'), ); }, ); diff --git a/example/lib/views/home_screen.dart b/example/lib/views/home_screen.dart index 4dd7e49..d271feb 100644 --- a/example/lib/views/home_screen.dart +++ b/example/lib/views/home_screen.dart @@ -145,3 +145,26 @@ class NavItem { this.icon, }); } + +class BlockiesAvatar extends StatelessWidget { + const BlockiesAvatar( + {super.key, required this.address, this.size = 60, this.radius = 12}); + final String address; + final double size; + final double radius; + + @override + Widget build(BuildContext context) { + return Container( + height: size, + width: size, + decoration: BoxDecoration( + shape: BoxShape.circle, + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(radius), + child: Blockies(seed: '${address}'), + ), + ); + } +} diff --git a/lib/src/chat/src/models/chat_members.dart b/lib/src/chat/src/models/chat_members.dart index c8ac267..136e654 100644 --- a/lib/src/chat/src/models/chat_members.dart +++ b/lib/src/chat/src/models/chat_members.dart @@ -1,24 +1,38 @@ import '../../../../push_restapi_dart.dart'; +class GroupMemberRole { + static const admin = "admin"; + static const member = "member"; + + static bool isValidRole(String role) { + return [member, admin].contains(role); + } +} + class ChatMemberProfile { String address; bool intent; String role; - UserV2 userInfo; + UserV2? userInfo; ChatMemberProfile({ required this.address, required this.intent, required this.role, required this.userInfo, - }); + }) { + if (!GroupMemberRole.isValidRole(role)) { + throw Exception('Invalid role'); + } + } factory ChatMemberProfile.fromJson(Map json) { return ChatMemberProfile( address: json['address'], intent: json['intent'], role: json['role'], - userInfo: UserV2.fromJson(json['userInfo']), + userInfo: + json['userInfo'] == null ? null : UserV2.fromJson(json['userInfo']), ); } @@ -27,7 +41,7 @@ class ChatMemberProfile { 'address': address, 'intent': intent, 'role': role, - 'userInfo': userInfo.toJson(), + 'userInfo': userInfo?.toJson(), }; } } diff --git a/lib/src/chat/src/models/group_info_dto.dart b/lib/src/chat/src/models/group_info_dto.dart index c345e05..4f0ec47 100644 --- a/lib/src/chat/src/models/group_info_dto.dart +++ b/lib/src/chat/src/models/group_info_dto.dart @@ -75,4 +75,8 @@ class GroupInfoDTO { 'encryptedSecret': encryptedSecret, }; } + + static GroupInfoDTO fromGroupDTO(GroupDTO group) { + return GroupInfoDTO.fromJson(group.toJson()); + } } diff --git a/lib/src/models/src/all.dart b/lib/src/models/src/all.dart index cc128a7..05259bb 100644 --- a/lib/src/models/src/all.dart +++ b/lib/src/models/src/all.dart @@ -285,12 +285,16 @@ class GroupDTO { } GroupDTO.fromJson(Map json) - : members = (json['members'] as List) - .map((member) => MemberDTO.fromJson(member)) - .toList(), - pendingMembers = (json['pendingMembers'] as List) - .map((member) => MemberDTO.fromJson(member)) - .toList(), + : members = json['members'] == null + ? [] + : (json['members'] as List) + .map((member) => MemberDTO.fromJson(member)) + .toList(), + pendingMembers = json['pendingMembers'] == null + ? [] + : (json['pendingMembers'] as List) + .map((member) => MemberDTO.fromJson(member)) + .toList(), contractAddressERC20 = json['contractAddressERC20'], numberOfERC20 = json['numberOfERC20'], contractAddressNFT = json['contractAddressNFT'], From a51c3ebc1e09c41a1977bbb04da4c397dbbb6a6d Mon Sep 17 00:00:00 2001 From: Gbogboade Ayomide Date: Fri, 15 Dec 2023 11:16:21 +0100 Subject: [PATCH 09/14] feat: disconnect socket when account logs out --- example/lib/views/account_provider.dart | 24 ++++++++++++------- .../views/group/requests/requests_screen.dart | 2 -- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/example/lib/views/account_provider.dart b/example/lib/views/account_provider.dart index 5e19566..90f9cd9 100644 --- a/example/lib/views/account_provider.dart +++ b/example/lib/views/account_provider.dart @@ -1,6 +1,6 @@ import '../__lib.dart'; import 'package:push_restapi_dart/push_restapi_dart.dart'; - +import 'package:socket_io_client/socket_io_client.dart' as io; import 'package:ethers/signers/wallet.dart' as ether; final accountProvider = ChangeNotifierProvider((ref) => AccountProvider(ref)); @@ -97,6 +97,7 @@ class AccountProvider extends ChangeNotifier { } } + io.Socket? pushSDKSocket; Future creatSocketConnection() async { try { final options = SocketInputOptions( @@ -109,26 +110,26 @@ class AccountProvider extends ChangeNotifier { ), ); - final pushSDKSocket = await createSocketConnection(options); + pushSDKSocket = await createSocketConnection(options); if (pushSDKSocket == null) { throw Exception('PushSDKSocket Connection Failed'); } - pushSDKSocket.connect(); + pushSDKSocket!.connect(); - pushSDKSocket.on( + pushSDKSocket!.on( EVENTS.CONNECT, (data) async { print(' NOTIFICATION EVENTS.CONNECT: $data'); }, ); // To get messages in realtime - pushSDKSocket.on(EVENTS.CHAT_RECEIVED_MESSAGE, (message) { + pushSDKSocket!.on(EVENTS.CHAT_RECEIVED_MESSAGE, (message) { print('CHAT NOTIFICATION EVENTS.CHAT_RECEIVED_MESSAGE: $message'); ref.read(conversationsProvider).onReceiveSocket(message); }); - pushSDKSocket.on(EVENTS.SPACES, (groupInfo) { + pushSDKSocket!.on(EVENTS.SPACES, (groupInfo) { print('SPACES NOTIFICATION EVENTS.SPACES: $groupInfo'); final type = (groupInfo as Map)['eventType']; @@ -149,7 +150,7 @@ class AccountProvider extends ChangeNotifier { }); // To get group creation or updation events - pushSDKSocket.on(EVENTS.CHAT_GROUPS, (groupInfo) { + pushSDKSocket!.on(EVENTS.CHAT_GROUPS, (groupInfo) { print('CHAT NOTIFICATION EVENTS.CHAT_GROUPS: $groupInfo'); final type = (groupInfo as Map)['eventType']; @@ -172,7 +173,7 @@ class AccountProvider extends ChangeNotifier { }); // To get realtime updates for spaces - pushSDKSocket.on( + pushSDKSocket!.on( EVENTS.SPACES_MESSAGES, (data) async { final message = data as Map; @@ -228,7 +229,7 @@ class AccountProvider extends ChangeNotifier { }, ); - pushSDKSocket.on( + pushSDKSocket!.on( EVENTS.DISCONNECT, (data) { print(' NOTIFICATION EVENTS.DISCONNECT: $data'); @@ -241,6 +242,11 @@ class AccountProvider extends ChangeNotifier { logOut() { pushWallet = null; + if (pushSDKSocket != null) { + pushSDKSocket!.close(); + pushSDKSocket = null; + } + notifyListeners(); } } diff --git a/example/lib/views/group/requests/requests_screen.dart b/example/lib/views/group/requests/requests_screen.dart index 07ae9a0..6e3c9db 100644 --- a/example/lib/views/group/requests/requests_screen.dart +++ b/example/lib/views/group/requests/requests_screen.dart @@ -52,9 +52,7 @@ class _ChatRequestScreenState extends ConsumerState { return ListTile( onTap: () { - // pushScreen(ChatRoomScreen(room: item)); onAccetRequests(item.chatId!); - // onAccetRequests(item.intentSentBy!); }, leading: ProfileImage(imageUrl: image), title: Text( From a7566b1d0faf63820452d90984cde5fba1b7e32a Mon Sep 17 00:00:00 2001 From: Gbogboade Ayomide Date: Fri, 15 Dec 2023 11:37:29 +0100 Subject: [PATCH 10/14] chore: fix file formatting --- example/lib/views/group/__group.dart | 2 +- example/lib/views/group/chat_room/chat_room_screen.dart | 3 ++- example/lib/views/spaces/live_space/live_space_provider.dart | 4 ++-- .../lib/views/spaces/live_space/space_moderation_dialog.dart | 3 ++- .../views/spaces/space_requests/space_request_provider.dart | 2 +- lib/src/spaces/src/helpers/is_host.dart | 2 +- lib/src/spaces/src/reject_promotion_invite.dart | 4 ++-- 7 files changed, 11 insertions(+), 9 deletions(-) diff --git a/example/lib/views/group/__group.dart b/example/lib/views/group/__group.dart index f710ed9..eed9654 100644 --- a/example/lib/views/group/__group.dart +++ b/example/lib/views/group/__group.dart @@ -10,4 +10,4 @@ export 'requests/request_provider.dart'; export 'create_group_screen.dart'; export 'group_members_dialog.dart'; export 'add_group_member.dart'; -export 'edit_group_info.screen.dart'; \ No newline at end of file +export 'edit_group_info.screen.dart'; diff --git a/example/lib/views/group/chat_room/chat_room_screen.dart b/example/lib/views/group/chat_room/chat_room_screen.dart index 591a182..3afaa63 100644 --- a/example/lib/views/group/chat_room/chat_room_screen.dart +++ b/example/lib/views/group/chat_room/chat_room_screen.dart @@ -70,7 +70,8 @@ class _ChatRoomScreenState extends ConsumerState { ), ) ], - title: Text('${roomVm.groupInformation?.groupName ?? room.intentSentBy}'), + title: + Text('${roomVm.groupInformation?.groupName ?? room.intentSentBy}'), ), body: GestureDetector( onTap: () { diff --git a/example/lib/views/spaces/live_space/live_space_provider.dart b/example/lib/views/spaces/live_space/live_space_provider.dart index 020b0b1..f932347 100644 --- a/example/lib/views/spaces/live_space/live_space_provider.dart +++ b/example/lib/views/spaces/live_space/live_space_provider.dart @@ -84,9 +84,9 @@ class LiveSpaceProvider extends PushSpaceNotifier { mainAxisAlignment: MainAxisAlignment.end, children: [ TextButton( - onPressed: ()async { + onPressed: () async { flushBar.dismiss(); - await rejectPromotionInvite(); + await rejectPromotionInvite(); }, child: KText( 'Reject', diff --git a/example/lib/views/spaces/live_space/space_moderation_dialog.dart b/example/lib/views/spaces/live_space/space_moderation_dialog.dart index 5adc88c..eea334e 100644 --- a/example/lib/views/spaces/live_space/space_moderation_dialog.dart +++ b/example/lib/views/spaces/live_space/space_moderation_dialog.dart @@ -290,7 +290,8 @@ class _SpaceListernersTab extends StatelessWidget { borderRadius: BorderRadius.circular(16)), onPressed: () { pop(); - Get.bottomSheet(SendSpaceInviteDialog(spaceId: spaceId, isSpeaker: false)); + Get.bottomSheet( + SendSpaceInviteDialog(spaceId: spaceId, isSpeaker: false)); }, textColor: Colors.white, child: Center(child: Text('Send Space Invite')), diff --git a/example/lib/views/spaces/space_requests/space_request_provider.dart b/example/lib/views/spaces/space_requests/space_request_provider.dart index 7fc48d2..8fa1efb 100644 --- a/example/lib/views/spaces/space_requests/space_request_provider.dart +++ b/example/lib/views/spaces/space_requests/space_request_provider.dart @@ -17,7 +17,7 @@ class SpaceRequestsProvider extends ChangeNotifier { notifyListeners(); } -Future loadRequests() async { + Future loadRequests() async { setBusy(true); _requests = await spaceRequests( toDecrypt: true, diff --git a/lib/src/spaces/src/helpers/is_host.dart b/lib/src/spaces/src/helpers/is_host.dart index 2b9b1a2..9c98f0f 100644 --- a/lib/src/spaces/src/helpers/is_host.dart +++ b/lib/src/spaces/src/helpers/is_host.dart @@ -1,6 +1,6 @@ import 'package:push_restapi_dart/push_restapi_dart.dart'; -/// Verifies if the local user's address matches the host's address. +/// Verifies if the local user's address matches the host's address. /// If not, throws an exception with a custom error message. isHost({required String hostAddress, required String errorMessage}) { final localAddress = getCachedWallet()!.address!; diff --git a/lib/src/spaces/src/reject_promotion_invite.dart b/lib/src/spaces/src/reject_promotion_invite.dart index 9f9a9cd..c1536be 100644 --- a/lib/src/spaces/src/reject_promotion_invite.dart +++ b/lib/src/spaces/src/reject_promotion_invite.dart @@ -1,12 +1,12 @@ import 'package:push_restapi_dart/push_restapi_dart.dart'; // This function rejects a promotion invite for a listener to become a speaker in a live space -Future rejectPromotionInvite_({required SpaceData spaceData}) async{ +Future rejectPromotionInvite_({required SpaceData spaceData}) async { // Get the local address from the cached wallet final localAddress = getCachedWallet()?.address; // Fire a user activity message to reject the invite as a listener address to become a speaker - await sendLiveSpaceData( + await sendLiveSpaceData( messageType: MessageType.USER_ACTIVITY, updatedLiveSpaceData: spaceData.liveSpaceData, content: CHAT.UA_LISTENER_REJECT_PROMOTION_INVITE, From b10a7192cc9d5fd03e1e0a415117103dc409ad8f Mon Sep 17 00:00:00 2001 From: Gbogboade Ayomide Date: Fri, 15 Dec 2023 12:43:48 +0100 Subject: [PATCH 11/14] chore: rename file for search spaces --- .../group/chat_room/chat_room_provider.dart | 3 ++- .../views/spaces/search_spaces.screen.dart | 27 +++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 example/lib/views/spaces/search_spaces.screen.dart diff --git a/example/lib/views/group/chat_room/chat_room_provider.dart b/example/lib/views/group/chat_room/chat_room_provider.dart index 1f245ba..d3b5189 100644 --- a/example/lib/views/group/chat_room/chat_room_provider.dart +++ b/example/lib/views/group/chat_room/chat_room_provider.dart @@ -77,13 +77,14 @@ class ChatRoomProvider extends ChangeNotifier { GroupInfoDTO? groupData, }) async { if (groupData != null && groupData.chatId == _currentChatid) { - _room.groupInformation = GroupDTO.fromJson(groupData.toJson()); + _groupInfoDTO = GroupInfoDTO.fromJson(groupData.toJson()); notifyListeners(); } getRoomMessages(); getLatestGroupInfo(); + getLatestGroupMembers(); } Future getRoomMessages() async { diff --git a/example/lib/views/spaces/search_spaces.screen.dart b/example/lib/views/spaces/search_spaces.screen.dart new file mode 100644 index 0000000..3daa290 --- /dev/null +++ b/example/lib/views/spaces/search_spaces.screen.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; + +class SearchSpacesScreen extends StatefulWidget { + const SearchSpacesScreen({super.key}); + + @override + State createState() => _SearchSpacesScreenState(); +} + +class _SearchSpacesScreenState extends State { + TextEditingController controller = TextEditingController(); + @override + Widget build(BuildContext context) { + return Scaffold( + body: SafeArea( + child: Padding( + padding: EdgeInsets.all(16.0), + child: Column( + children: [ + + ], + ), + ), + ), + ); + } +} From c97e7416db8ab224831b4a030021f2ffac708645 Mon Sep 17 00:00:00 2001 From: Gbogboade Ayomide Date: Sat, 16 Dec 2023 07:51:12 +0100 Subject: [PATCH 12/14] fix: show byes image in chat screen --- example/lib/views/group/chat_room/chat_room_provider.dart | 3 ++- example/lib/views/group/chat_room/chat_room_screen.dart | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/example/lib/views/group/chat_room/chat_room_provider.dart b/example/lib/views/group/chat_room/chat_room_provider.dart index d3b5189..70fc1c1 100644 --- a/example/lib/views/group/chat_room/chat_room_provider.dart +++ b/example/lib/views/group/chat_room/chat_room_provider.dart @@ -160,7 +160,8 @@ class ChatRoomProvider extends ChangeNotifier { if (selectedFile != null && messageType == MessageType.IMAGE) { final img = base64Encode(selectedFile!.readAsBytesSync()); - attachmentContent = jsonEncode({'content': img}); + attachmentContent = + jsonEncode({'content': 'data:image/png;base64,$img'}); messageAttachment = ImageMessage( content: img, name: selectedFile?.uri.pathSegments.last, diff --git a/example/lib/views/group/chat_room/chat_room_screen.dart b/example/lib/views/group/chat_room/chat_room_screen.dart index 3afaa63..97ba5f0 100644 --- a/example/lib/views/group/chat_room/chat_room_screen.dart +++ b/example/lib/views/group/chat_room/chat_room_screen.dart @@ -477,8 +477,9 @@ class _ChatImage extends StatelessWidget { return Image.network(imageUrl!); } + final bytes = imageUrl!.split('base64,').last; return Image.memory( - dataFromBase64String(imageUrl!), + dataFromBase64String(bytes), fit: BoxFit.fill, ); } catch (e) { From 99525b421b299a6e5b031234b0ff664c1d5d924b Mon Sep 17 00:00:00 2001 From: Gbogboade Ayomide Date: Mon, 18 Dec 2023 11:45:16 +0100 Subject: [PATCH 13/14] feat: add search by space name --- example/lib/views/spaces/__spaces.dart | 1 + .../views/spaces/search_spaces.screen.dart | 71 ++++++++++++++++++- example/lib/views/spaces_tab.dart | 10 +++ lib/src/chat/src/send.dart | 2 +- lib/src/spaces/space.dart | 1 + lib/src/spaces/src/search.dart | 26 +++++++ 6 files changed, 109 insertions(+), 2 deletions(-) create mode 100644 lib/src/spaces/src/search.dart diff --git a/example/lib/views/spaces/__spaces.dart b/example/lib/views/spaces/__spaces.dart index f0e1474..c47c274 100644 --- a/example/lib/views/spaces/__spaces.dart +++ b/example/lib/views/spaces/__spaces.dart @@ -17,3 +17,4 @@ export 'popular/popular_space_provider.dart'; export 'popular/popular_tab.dart'; export 'your_spaces/for_you_tab.dart'; +export 'search_spaces.screen.dart'; diff --git a/example/lib/views/spaces/search_spaces.screen.dart b/example/lib/views/spaces/search_spaces.screen.dart index 3daa290..ae323b1 100644 --- a/example/lib/views/spaces/search_spaces.screen.dart +++ b/example/lib/views/spaces/search_spaces.screen.dart @@ -1,4 +1,6 @@ +import 'package:example/__lib.dart'; import 'package:flutter/material.dart'; +import 'package:push_restapi_dart/push_restapi_dart.dart'; class SearchSpacesScreen extends StatefulWidget { const SearchSpacesScreen({super.key}); @@ -9,6 +11,36 @@ class SearchSpacesScreen extends StatefulWidget { class _SearchSpacesScreenState extends State { TextEditingController controller = TextEditingController(); + + List _spaces = []; + + bool isSearching = false; + + Future _searchSpace() async { + try { + final term = controller.text.trim(); + if (term.isEmpty) { + setState(() { + _spaces.clear(); + isSearching = false; + }); + + return; + } + setState(() { + isSearching = true; + }); + final result = await searchSpaces(searchTerm: term); + + setState(() { + _spaces = result; + isSearching = false; + }); + } catch (e) { + showErrorSnackbar(e.toString()); + } + } + @override Widget build(BuildContext context) { return Scaffold( @@ -16,8 +48,45 @@ class _SearchSpacesScreenState extends State { child: Padding( padding: EdgeInsets.all(16.0), child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - + InkWell( + onTap: pop, + child: Icon( + Icons.close, + color: Colors.black, + ), + ), + SizedBox(height: 8), + SearchBar( + controller: controller, + hintText: 'enter space name', + trailing: [ + IconButton( + onPressed: isSearching + ? null + : () { + _searchSpace(); + }, + icon: Icon(Icons.search), + ) + ], + ), + SizedBox(height: 16), + Expanded( + child: isSearching + ? Center(child: LoadingDialog()) + : ListView.separated( + separatorBuilder: (context, index) => + SizedBox(height: 16), + itemCount: _spaces.length, + itemBuilder: (context, index) { + final item = SpaceFeeds( + spaceId: _spaces[index].spaceId, + spaceInformation: _spaces[index]); + return SpaceItemTile(item: item); + }, + )) ], ), ), diff --git a/example/lib/views/spaces_tab.dart b/example/lib/views/spaces_tab.dart index 23076c3..9b4cb9d 100644 --- a/example/lib/views/spaces_tab.dart +++ b/example/lib/views/spaces_tab.dart @@ -18,6 +18,16 @@ class _SpacesTabState extends ConsumerState { Row( mainAxisAlignment: MainAxisAlignment.end, children: [ + InkWell( + onTap: () { + pushScreen(SearchSpacesScreen()); + }, + child: Icon( + Icons.search, + color: pushColor, + ), + ), + Spacer(), InkWell( onTap: () { pushScreen(CreateSpaceScreen()); diff --git a/lib/src/chat/src/send.dart b/lib/src/chat/src/send.dart index 8001242..61b4167 100644 --- a/lib/src/chat/src/send.dart +++ b/lib/src/chat/src/send.dart @@ -46,7 +46,7 @@ Future send(ChatSendOptions options) async { } final sendMessagePayload = await sendMessagePayloadCore( - senderConnectedUser: connectedUser!, + senderConnectedUser: connectedUser, receiverAddress: computedOptions.to, senderPgpPrivateKey: computedOptions.pgpPrivateKey, senderAddress: computedOptions.account, diff --git a/lib/src/spaces/space.dart b/lib/src/spaces/space.dart index 4ab275d..54eed84 100644 --- a/lib/src/spaces/space.dart +++ b/lib/src/spaces/space.dart @@ -9,6 +9,7 @@ export 'src/start.dart'; export 'src/update.dart'; export 'src/trending.dart'; export 'src/approve.dart'; +export 'src/search.dart'; export 'src/space.dart'; export 'src/request_to_be_promoted.dart'; diff --git a/lib/src/spaces/src/search.dart b/lib/src/spaces/src/search.dart new file mode 100644 index 0000000..3353e03 --- /dev/null +++ b/lib/src/spaces/src/search.dart @@ -0,0 +1,26 @@ +import 'package:push_restapi_dart/push_restapi_dart.dart'; + +Future> searchSpaces({ + required String searchTerm, + int pageNumber = 1, + int pageSize = 20, +}) async { + try { + final body = { + 'searchTerm': searchTerm, + 'pageNumber': pageNumber, + 'pageSize': pageSize, + }; + + final result = await http.post(path: '/v1/spaces/search', data: body); + + if (result == null || result is String) { + throw Exception(result ?? 'Cannot find spaces that match $searchTerm'); + } + + return (result as List).map((e) => SpaceDTO.fromJson(e)).toList(); + } catch (e) { + log('[Push SDK] - API - Error - API searchSpaces -: $e'); + rethrow; + } +} From fb678a868c37dea9395657472f971b256c5f56b6 Mon Sep 17 00:00:00 2001 From: Gbogboade Ayomide Date: Mon, 15 Jan 2024 12:04:47 +0100 Subject: [PATCH 14/14] feat: add search screen --- example/demo_app/ios/Podfile.lock | 31 ++++---- .../ios/Runner.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/xcschemes/Runner.xcscheme | 2 +- .../views/spaces/search_spaces.screen.dart | 74 ++++++++++++++++++- 4 files changed, 90 insertions(+), 19 deletions(-) diff --git a/example/demo_app/ios/Podfile.lock b/example/demo_app/ios/Podfile.lock index a5e0ed1..ff43ff7 100644 --- a/example/demo_app/ios/Podfile.lock +++ b/example/demo_app/ios/Podfile.lock @@ -9,15 +9,15 @@ PODS: - Flutter - flutter_webrtc (0.9.36): - Flutter - - WebRTC-SDK (= 114.5735.02) + - WebRTC-SDK (= 114.5735.08) - image_cropper (0.0.4): - Flutter - TOCropViewController (~> 2.6.1) - image_picker_ios (0.0.1): - Flutter - - livekit_client (1.4.0): + - livekit_client (1.5.6): - Flutter - - WebRTC-SDK (= 114.5735.02) + - WebRTC-SDK (= 114.5735.08) - openpgp (0.6.0): - Flutter - path_provider_foundation (0.0.1): @@ -29,7 +29,8 @@ PODS: - TOCropViewController (2.6.1) - video_player_avfoundation (0.0.1): - Flutter - - WebRTC-SDK (114.5735.02) + - FlutterMacOS + - WebRTC-SDK (114.5735.08) DEPENDENCIES: - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) @@ -43,7 +44,7 @@ DEPENDENCIES: - openpgp (from `.symlinks/plugins/openpgp/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) - - video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/ios`) + - video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`) SPEC REPOS: trunk: @@ -75,24 +76,24 @@ EXTERNAL SOURCES: permission_handler_apple: :path: ".symlinks/plugins/permission_handler_apple/ios" video_player_avfoundation: - :path: ".symlinks/plugins/video_player_avfoundation/ios" + :path: ".symlinks/plugins/video_player_avfoundation/darwin" SPEC CHECKSUMS: - connectivity_plus: 07c49e96d7fc92bc9920617b83238c4d178b446a - device_info_plus: 7545d84d8d1b896cb16a4ff98c19f07ec4b298ea + connectivity_plus: bf0076dd84a130856aa636df1c71ccaff908fa1d + device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6 Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 flutter_native_image: 9c0b7451838484458e5b0fae007b86a4c2d4bdfe - flutter_webrtc: 1944895d4e908c4bc722929dc4b9f8620d8e1b2f + flutter_webrtc: 55df3aaa802114dad390191a46c2c8d535751268 image_cropper: a3291c624a953049bc6a02e1f8c8ceb162a24b25 - image_picker_ios: 4a8aadfbb6dc30ad5141a2ce3832af9214a705b5 - livekit_client: dedfd6601bfb2c567d6988491c8bab7ceeb5a6d9 - openpgp: 7d926bdb9e5544b1ae69a7a051716c36a3271b38 - path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 + image_picker_ios: 99dfe1854b4fa34d0364e74a78448a0151025425 + livekit_client: 381ed3cad6ba0b6ffbfffcf1b5db95ad1e61ffa2 + openpgp: 117b855c299b1e74f9f58fc027adf456aaac09c2 + path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6 ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 TOCropViewController: edfd4f25713d56905ad1e0b9f5be3fbe0f59c863 - video_player_avfoundation: 81e49bb3d9fb63dccf9fa0f6d877dc3ddbeac126 - WebRTC-SDK: dd913fd31cfbf1d43b9a22d83f4c6354c960c623 + video_player_avfoundation: e9e6f9cae7d7a6d9b43519b0aab382bca60fcfd1 + WebRTC-SDK: c24d2a6c9f571f2ed42297cb8ffba9557093142b PODFILE CHECKSUM: f9420bd595da8fbce156b547dcd3368afc5226ff diff --git a/example/demo_app/ios/Runner.xcodeproj/project.pbxproj b/example/demo_app/ios/Runner.xcodeproj/project.pbxproj index 8e89e93..ef6b253 100644 --- a/example/demo_app/ios/Runner.xcodeproj/project.pbxproj +++ b/example/demo_app/ios/Runner.xcodeproj/project.pbxproj @@ -214,7 +214,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1300; + LastUpgradeCheck = 1430; ORGANIZATIONNAME = ""; TargetAttributes = { 331C8080294A63A400263BE5 = { diff --git a/example/demo_app/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/demo_app/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index e42adcb..87131a0 100644 --- a/example/demo_app/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/example/demo_app/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ { TextEditingController controller = TextEditingController(); + + List _spaces = []; + + bool isSearching = false; + + Future _searchSpace() async { + try { + final term = controller.text.trim(); + if (term.isEmpty) { + setState(() { + _spaces.clear(); + isSearching = false; + }); + + return; + } + setState(() { + isSearching = true; + }); + final result = await searchSpaces(searchTerm: term); + + setState(() { + _spaces = result; + isSearching = false; + }); + } catch (e) { + showErrorSnackbar(e.toString()); + } + } + @override Widget build(BuildContext context) { return Scaffold( @@ -16,7 +47,46 @@ class _SearchSpacesScreenState extends State { child: Padding( padding: EdgeInsets.all(16.0), child: Column( - children: [], + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + InkWell( + onTap: pop, + child: Icon( + Icons.close, + color: Colors.black, + ), + ), + SizedBox(height: 8), + SearchBar( + controller: controller, + hintText: 'enter space name', + trailing: [ + IconButton( + onPressed: isSearching + ? null + : () { + _searchSpace(); + }, + icon: Icon(Icons.search), + ) + ], + ), + SizedBox(height: 16), + Expanded( + child: isSearching + ? Center(child: LoadingDialog()) + : ListView.separated( + separatorBuilder: (context, index) => + SizedBox(height: 16), + itemCount: _spaces.length, + itemBuilder: (context, index) { + final item = SpaceFeeds( + spaceId: _spaces[index].spaceId, + spaceInformation: _spaces[index]); + return SpaceItemTile(item: item); + }, + )) + ], ), ), ),