diff --git a/test/.DS_Store b/.DS_Store similarity index 92% rename from test/.DS_Store rename to .DS_Store index 010881a..dc2676f 100644 Binary files a/test/.DS_Store and b/.DS_Store differ 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 @@ decryptPGPKey({ } //TODO: Implement verifyProfileKeys - verifyProfileKeys({ +verifyProfileKeys({ required String encryptedPrivateKey, required String publicKey, required String did, diff --git a/lib/src/models/src/all.dart b/lib/src/models/src/all.dart index cb2f02d..a53e1b7 100644 --- a/lib/src/models/src/all.dart +++ b/lib/src/models/src/all.dart @@ -149,12 +149,12 @@ class AdditionalMeta { } } -class NotificationPayload { +class NotificationApiPayload { NotificationOptions notification; PayloadData data; dynamic recipients; - NotificationPayload({ + NotificationApiPayload({ required this.notification, required this.data, required this.recipients, diff --git a/lib/src/payloads/src/helpers.dart b/lib/src/payloads/src/helpers.dart index a806c0e..68466de 100644 --- a/lib/src/payloads/src/helpers.dart +++ b/lib/src/payloads/src/helpers.dart @@ -8,10 +8,10 @@ String getUUID() { return uuid.v4(); } -NotificationPayload? getPayloadForAPIInput( +NotificationApiPayload? getPayloadForAPIInput( SendNotificationInputOptions inputOptions, dynamic recipients) { if (inputOptions.notification != null && inputOptions.payload != null) { - return NotificationPayload( + return NotificationApiPayload( notification: NotificationOptions( title: inputOptions.notification?.title ?? '', body: inputOptions.notification?.body ?? '', @@ -117,7 +117,7 @@ Future getVerificationProof({ required NOTIFICATION_TYPE notificationType, required IDENTITY_TYPE identityType, required String verifyingContract, - required NotificationPayload? payload, + required NotificationApiPayload? payload, String? ipfsHash, Map graph = const {}, required String uuid, @@ -197,7 +197,7 @@ Future getVerificationProof({ String getPayloadIdentity({ required IDENTITY_TYPE identityType, - required NotificationPayload? payload, + required NotificationApiPayload? payload, NOTIFICATION_TYPE? notificationType, String? ipfsHash, Graph? graph, diff --git a/lib/src/pushapi/__pushapi.dart b/lib/src/pushapi/__pushapi.dart deleted file mode 100644 index af5402b..0000000 --- a/lib/src/pushapi/__pushapi.dart +++ /dev/null @@ -1,4 +0,0 @@ -export 'models.dart'; -export 'pushapi.dart'; -export 'chat.dart' hide GroupAPI, GroupParticipantsAPI; -export 'user.dart'; diff --git a/lib/src/pushapi/pushapi.dart b/lib/src/pushapi/pushapi.dart index d446d55..5309264 100644 --- a/lib/src/pushapi/pushapi.dart +++ b/lib/src/pushapi/pushapi.dart @@ -1,117 +1,4 @@ -import '../../push_restapi_dart.dart'; - -class PushAPI { - late final Signer? _signer; - late final String _account; - late final String? _decryptedPgpPvtKey; - final String? pgpPublicKey; - final bool readMode; - - void Function(ProgressHookType)? progressHook; - - late Chat chat; - PushAPI({ - Signer? signer, - required String account, - String? decryptedPgpPvtKey, - this.pgpPublicKey, - this.progressHook, - this.readMode = false, - ENV env = ENV.staging, - bool showHttpLog = false, - }) { - _signer = signer; - _account = account; - _decryptedPgpPvtKey = decryptedPgpPvtKey; - - initPush( - env: env, - showHttpLog: showHttpLog, - ); - - chat = Chat( - signer: _signer, - account: _account, - decryptedPgpPvtKey: _decryptedPgpPvtKey, - pgpPublicKey: pgpPublicKey, - progressHook: progressHook, - ); - } - - static Future initialize( - {Signer? signer, PushAPIInitializeOptions? options}) async { - if (signer == null && options?.account == null) { - throw Exception("Either 'signer' or 'account' must be provided."); - } - - final readMode = signer != null; - - // Get account - // Derives account from signer if not provided - String? derivedAccount; - - if (signer != null) { - derivedAccount = getAccountAddress( - getWallet(address: options?.account, signer: signer)); - } else { - derivedAccount = options?.account; - } - - if (derivedAccount == null) { - throw Exception('Account could not be derived.'); - } - - String? decryptedPGPPrivateKey; - String? pgpPublicKey; - - /** - * Decrypt PGP private key - * If user exists, decrypts the PGP private key - * If user does not exist, creates a new user and returns the decrypted PGP private key - */ - final user = await getUser(address: derivedAccount); - - if (readMode) { - if (user != null && user.encryptedPrivateKey != null) { - decryptedPGPPrivateKey = await decryptPGPKey( - toUpgrade: options?.autoUpgrade, - progressHook: options?.progressHook, - additionalMeta: options?.versionMeta, - encryptedPGPPrivateKey: user.encryptedPrivateKey!, - wallet: getWallet(address: options?.account, signer: signer), - ); - pgpPublicKey = user.publicKey; - } else { - final newUser = await createUser( - signer: signer, - progressHook: options?.progressHook ?? (_) {}, - version: options?.version ?? ENCRYPTION_TYPE.PGP_V3, - ); - decryptedPGPPrivateKey = newUser.decryptedPrivateKey; - pgpPublicKey = newUser.publicKey; - } - } - - final api = PushAPI( - account: derivedAccount, - signer: signer, - decryptedPgpPvtKey: decryptedPGPPrivateKey, - pgpPublicKey: pgpPublicKey, - readMode: readMode, - showHttpLog: options?.showHttpLog ?? false, - ); - - return api; - } - -//TODO initStream - Future initStream() async {} - - Future info({String? overrideAccount}) async { - return getUser(address: overrideAccount ?? _account); - } - - static String ensureSignerMessage() { - return 'Operation not allowed in read-only mode. Signer is required.'; - } -} +export 'src/models.dart'; +export 'src/pushapi.dart'; +export 'src/chat.dart' hide GroupAPI, GroupParticipantsAPI; +export 'src/user.dart'; diff --git a/lib/src/pushapi/chat.dart b/lib/src/pushapi/src/chat.dart similarity index 99% rename from lib/src/pushapi/chat.dart rename to lib/src/pushapi/src/chat.dart index 10049bd..772ce83 100644 --- a/lib/src/pushapi/chat.dart +++ b/lib/src/pushapi/src/chat.dart @@ -1,8 +1,8 @@ // ignore_for_file: library_prefixes -import '../../push_restapi_dart.dart'; -import '../chat/chat.dart' as PUSH_CHAT; -import '../user/user.dart' as PUSH_USER; +import '../../../push_restapi_dart.dart'; +import '../../chat/chat.dart' as PUSH_CHAT; +import '../../user/user.dart' as PUSH_USER; class Chat { late final Signer? _signer; diff --git a/lib/src/pushapi/models.dart b/lib/src/pushapi/src/models.dart similarity index 97% rename from lib/src/pushapi/models.dart rename to lib/src/pushapi/src/models.dart index 5c84c32..7033ce3 100644 --- a/lib/src/pushapi/models.dart +++ b/lib/src/pushapi/src/models.dart @@ -1,6 +1,6 @@ // ignore_for_file: constant_identifier_names -import '../../push_restapi_dart.dart'; +import '../../../push_restapi_dart.dart'; class PushAPIInitializeOptions { void Function(ProgressHookType)? progressHook; diff --git a/lib/src/pushapi/src/pushapi.dart b/lib/src/pushapi/src/pushapi.dart new file mode 100644 index 0000000..ac93836 --- /dev/null +++ b/lib/src/pushapi/src/pushapi.dart @@ -0,0 +1,132 @@ +import '../../../push_restapi_dart.dart'; + +class PushAPI { + late final Signer? _signer; + late final String _account; + late final String? _decryptedPgpPvtKey; + final String? pgpPublicKey; + final bool readMode; + + void Function(ProgressHookType)? progressHook; + + late Chat chat; + late PushStream stream; + PushAPI({ + Signer? signer, + required String account, + String? decryptedPgpPvtKey, + this.pgpPublicKey, + this.progressHook, + this.readMode = false, + ENV env = ENV.staging, + bool showHttpLog = false, + }) { + _signer = signer; + _account = account; + _decryptedPgpPvtKey = decryptedPgpPvtKey; + + initPush( + env: env, + showHttpLog: showHttpLog, + ); + + chat = Chat( + signer: _signer, + account: _account, + decryptedPgpPvtKey: _decryptedPgpPvtKey, + pgpPublicKey: pgpPublicKey, + progressHook: progressHook, + ); + } + + static Future initialize({ + Signer? signer, + PushAPIInitializeOptions? options, + }) async { + if (signer == null && options?.account == null) { + throw Exception("Either 'signer' or 'account' must be provided."); + } + + final readMode = signer != null; + + // Get account + // Derives account from signer if not provided + String? derivedAccount; + + if (signer != null) { + derivedAccount = getAccountAddress( + getWallet(address: options?.account, signer: signer)); + } else { + derivedAccount = options?.account; + } + + if (derivedAccount == null) { + throw Exception('Account could not be derived.'); + } + + String? decryptedPGPPrivateKey; + String? pgpPublicKey; + + /** + * Decrypt PGP private key + * If user exists, decrypts the PGP private key + * If user does not exist, creates a new user and returns the decrypted PGP private key + */ + final user = await getUser(address: derivedAccount); + + if (readMode) { + if (user != null && user.encryptedPrivateKey != null) { + decryptedPGPPrivateKey = await decryptPGPKey( + toUpgrade: options?.autoUpgrade, + progressHook: options?.progressHook, + additionalMeta: options?.versionMeta, + encryptedPGPPrivateKey: user.encryptedPrivateKey!, + wallet: getWallet(address: options?.account, signer: signer), + ); + pgpPublicKey = user.publicKey; + } else { + final newUser = await createUser( + signer: signer, + progressHook: options?.progressHook ?? (_) {}, + version: options?.version ?? ENCRYPTION_TYPE.PGP_V3, + ); + decryptedPGPPrivateKey = newUser.decryptedPrivateKey; + pgpPublicKey = newUser.publicKey; + } + } + + final api = PushAPI( + account: derivedAccount, + signer: signer, + decryptedPgpPvtKey: decryptedPGPPrivateKey, + pgpPublicKey: pgpPublicKey, + readMode: readMode, + showHttpLog: options?.showHttpLog ?? false, + ); + + return api; + } + + Future initStream( + {required List listen, + PushStreamInitializeOptions? options}) async { + stream = await PushStream.initialize( + account: _account, + listen: listen, + decryptedPgpPvtKey: _decryptedPgpPvtKey, + options: options, + progressHook: progressHook, + signer: _signer, + ); + + return stream; + } + + Future info({String? overrideAccount}) async { + return getUser(address: overrideAccount ?? _account); + } + + static String ensureSignerMessage() { + return 'Operation not allowed in read-only mode. Signer is required.'; + } +} diff --git a/lib/src/pushapi/src/user.dart b/lib/src/pushapi/src/user.dart new file mode 100644 index 0000000..2ca8eb6 --- /dev/null +++ b/lib/src/pushapi/src/user.dart @@ -0,0 +1,14 @@ +import '../../../push_restapi_dart.dart'; + +class UserAPI { + late final String _account; + UserAPI({ + required String account, + }) { + _account = account; + } + + Future info({String? overrideAccount}) async { + return getUser(address: overrideAccount ?? _account); + } +} diff --git a/lib/src/pushstream/push_stream.dart b/lib/src/pushstream/push_stream.dart new file mode 100644 index 0000000..f956b52 --- /dev/null +++ b/lib/src/pushstream/push_stream.dart @@ -0,0 +1,4 @@ +export 'src/push_stream.dart'; +export 'src/models/models.dart'; +export 'src/models/notification_model.dart'; +export 'src/data_modifier.dart'; diff --git a/lib/src/pushstream/src/data_modifier.dart b/lib/src/pushstream/src/data_modifier.dart new file mode 100644 index 0000000..7352b8b --- /dev/null +++ b/lib/src/pushstream/src/data_modifier.dart @@ -0,0 +1,178 @@ +import '../../../push_restapi_dart.dart'; + +class DataModifier { + static Future handleChatGroupEvent( + {required dynamic data, bool includeRaw = false}) async { + switch (data['eventType']) { + case 'create': + break; + default: + } + } + + static ProposedEventNames convertToProposedName(String currentEventName) { + switch (currentEventName) { + case 'message': + return ProposedEventNames.Message; + case 'request': + return ProposedEventNames.Request; + case 'accept': + return ProposedEventNames.Accept; + case 'reject': + return ProposedEventNames.Reject; + case 'leaveGroup': + return ProposedEventNames.LeaveGroup; + case 'joinGroup': + return ProposedEventNames.JoinGroup; + case 'createGroup': + return ProposedEventNames.CreateGroup; + case 'updateGroup': + return ProposedEventNames.UpdateGroup; + case 'remove': + return ProposedEventNames.Remove; + default: + throw Exception('Unknown current event name: $currentEventName'); + } + } + + static handleToField(dynamic data) { + switch (data.event) { + case ProposedEventNames.LeaveGroup: + case ProposedEventNames.JoinGroup: + data.to = null; + break; + + case ProposedEventNames.Accept: + case ProposedEventNames.Reject: + if (data['meta']?['group'] != null) { + data.to = null; + } + break; + + default: + break; + } + } + + static handleChatEvent(dynamic data, [includeRaw = false]) async { + if (data == null) { + log('Error in handleChatEvent: data is undefined or null'); + throw Exception('data is undefined or null'); + } + + final eventTypeMap = { + 'Chat': MessageEventType.message, + 'Request': MessageEventType.request, + 'Approve': MessageEventType.accept, + 'Reject': MessageEventType.reject, + }; + + final key = data['eventType'] ?? data['messageCategory']; + + if (!eventTypeMap.containsKey(key)) { + throw FormatException('Invalid eventType or messageCategory in data'); + } + + final eventType = eventTypeMap[key]; + + if (eventType != null) { + return mapToMessageEvent(data, includeRaw, eventType); + } else { + log('Unknown eventType: ${data['eventType'] ?? data['messageCategory']}'); + return data; + } + } + + static MessageEvent mapToMessageEvent( + Map data, + bool includeRaw, + String eventType, + ) { + final messageEvent = MessageEvent( + event: eventType, + origin: data['messageOrigin'], + timestamp: data['timestamp'].toString(), + chatId: data['chatId'], + from: data['fromCAIP10'], + to: [data['toCAIP10']], + message: MessageContent( + type: data['messageType'], + content: data['messageContent'], + ), + meta: MessageMeta(group: data['isGroup'] ?? false), + reference: data['cid'], + raw: includeRaw ? MessageRawData.fromJson(data) : null, + ); + + return messageEvent; + } + + static NotificationEvent mapToNotificationEvent( + {required dynamic data, + required String notificationEventType, + required String origin, + includeRaw = false}) { + final notificationType = NOTIFICATION_TYPE_MAP.keys.firstWhere( + (key) => NOTIFICATION_TYPE_MAP[key] == data['payload']['data']['type'], + orElse: () => 'BROADCAST', + ); + + List recipients; + + if (data['payload']['recipients'] is List) { + recipients = List.from(data['payload']['recipients']); + } else if (data['payload']['recipients'] is String) { + recipients = [data['payload']['recipients']]; + } else { + recipients = data['payload']['recipients'].keys.toList(); + } + + final notificationEvent = NotificationEvent( + event: notificationEventType, + origin: origin, + timestamp: data['epoch'].toString(), + from: data['sender'], + to: recipients, + notifID: data['payload_id'].toString(), + channel: NotificationChannel( + name: data['payload']['data']['app'], + icon: data['payload']['data']['icon'], + url: data['payload']['data']['url'], + ), + meta: NotificationMeta( + type: 'NOTIFICATION.$notificationType', + ), + message: NotificationMessage( + notification: NotificationContent( + title: data['payload']['notification']['title'], + body: data['payload']['notification']['body'], + ), + payload: NotificationPayload( + title: data['payload']['data']['asub'], + body: data['payload']['data']['amsg'], + cta: data['payload']['data']['acta'], + embed: data['payload']['data']['aimg'], + meta: NotificationPayloadMeta( + domain: data['payload']['data']['additionalMeta']['domain'] ?? + 'push.org', + type: data['payload']['data']['additionalMeta']['type'], + data: data['payload']['data']['additionalMeta']['data'], + ), + ), + ), + config: NotificationConfig( + expiry: data['payload']['data']['etime'], + silent: data['payload']['data']['silent'] == '1', + hidden: data['payload']['data']['hidden'] == '1', + ), + source: data['source'], + raw: includeRaw + ? NotificationRawData( + verificationProof: data['payload']['verificationProof'], + ) + : null, + ); + + return notificationEvent; + } +} diff --git a/lib/src/pushstream/src/models/models.dart b/lib/src/pushstream/src/models/models.dart new file mode 100644 index 0000000..136cfcc --- /dev/null +++ b/lib/src/pushstream/src/models/models.dart @@ -0,0 +1,191 @@ +// ignore_for_file: constant_identifier_names + +import '../../../../push_restapi_dart.dart'; + +class PushStreamInitializeOptions { + final PushStreamFilter? filter; + final PushStreamConnection? connection; + final bool raw; + final ENV env; + final String? overrideAccount; + + PushStreamInitializeOptions({ + this.filter, + this.connection, + this.raw = false, + this.overrideAccount, + this.env = ENV.staging, + }); + + static PushStreamInitializeOptions defaut() { + return PushStreamInitializeOptions(connection: PushStreamConnection()); + } +} + +class PushStreamFilter { + final List? channels; + final List? chats; + + PushStreamFilter({this.channels, this.chats}); +} + +class PushStreamConnection { + final bool auto; + final int retries; + + PushStreamConnection({ + this.auto = true, + this.retries = 3, + }); +} + +enum STREAM { + PROFILE, + ENCRYPTION, + NOTIF, + NOTIF_OPS, + CHAT, + CHAT_OPS, + CONNECT, + DISCONNECT, +} + +extension STREAMExtension on STREAM { + String get value { + switch (this) { + case STREAM.PROFILE: + return 'STREAM.PROFILE'; + case STREAM.ENCRYPTION: + return 'STREAM.ENCRYPTION'; + case STREAM.NOTIF: + return 'STREAM.NOTIF'; + case STREAM.NOTIF_OPS: + return 'STREAM.NOTIF_OPS'; + case STREAM.CHAT: + return 'STREAM.CHAT'; + case STREAM.CHAT_OPS: + return 'STREAM.CHAT_OPS'; + case STREAM.CONNECT: + return 'STREAM.CONNECT'; + case STREAM.DISCONNECT: + return 'STREAM.DISCONNECT'; + } + } +} + +enum ProposedEventNames { + Message, + Request, + Accept, + Reject, + LeaveGroup, + JoinGroup, + CreateGroup, + UpdateGroup, + Remove, +} + +class GroupEventType { + static final createGroup = 'createGroup'; + static final updateGroup = 'updateGroup'; + static final joinGroup = 'joinGroup'; + static final leaveGroup = 'leaveGroup'; + static final remove = 'remove'; +} + +class MessageEventType { + static final message = 'message'; + static final request = 'request'; + static final accept = 'accept'; + static final reject = 'reject'; +} + +class MessageEvent { + final String event; + final String origin; + final String timestamp; + final String chatId; + final String from; + final List to; + final MessageContent message; + final MessageMeta meta; + final String reference; + final MessageRawData? raw; + + MessageEvent({ + required this.event, + required this.origin, + required this.timestamp, + required this.chatId, + required this.from, + required this.to, + required this.message, + required this.meta, + required this.reference, + this.raw, + }); +} + +class MessageContent { + final String type; + final String content; + + MessageContent({ + required this.type, + required this.content, + }); +} + +class MessageMeta { + final bool group; + + MessageMeta({ + required this.group, + }); +} + +class MessageRawData { + final String fromCAIP10; + final String toCAIP10; + final String fromDID; + final String toDID; + final String encType; + final String encryptedSecret; + final String signature; + final String sigType; + final String verificationProof; + final String previousReference; + + MessageRawData({ + required this.fromCAIP10, + required this.toCAIP10, + required this.fromDID, + required this.toDID, + required this.encType, + required this.encryptedSecret, + required this.signature, + required this.sigType, + required this.verificationProof, + required this.previousReference, + }); + + factory MessageRawData.fromJson(Map data) { + return MessageRawData( + fromCAIP10: data['fromCAIP10'], + toCAIP10: data['toCAIP10'], + fromDID: data['fromDID'], + toDID: data['toDID'], + encType: data['encType'], + encryptedSecret: data['encryptedSecret'], + signature: data['signature'], + sigType: data['sigType'], + verificationProof: data['verificationProof'], + previousReference: data['link'], + ); + } +} + +class MessageOrigin { + static const other = 'other'; + static const self = 'self'; +} diff --git a/lib/src/pushstream/src/models/notification_model.dart b/lib/src/pushstream/src/models/notification_model.dart new file mode 100644 index 0000000..dccd9b1 --- /dev/null +++ b/lib/src/pushstream/src/models/notification_model.dart @@ -0,0 +1,140 @@ +// ignore_for_file: constant_identifier_names + +const Map NOTIFICATION_TYPE_MAP = { + 'BROADCAST': 1, + 'TARGETTED': 3, + 'SUBSET': 4, +}; + +class NotificationEvent { + final String event; + String origin; + final String timestamp; + final String from; + final List to; + final String notifID; + final NotificationChannel channel; + final NotificationMeta meta; + final NotificationMessage message; + final NotificationConfig? config; + final NotificationAdvanced? advanced; + final String source; + final NotificationRawData? raw; + + NotificationEvent({ + required this.event, + required this.origin, + required this.timestamp, + required this.from, + required this.to, + required this.notifID, + required this.channel, + required this.meta, + required this.message, + this.config, + this.advanced, + required this.source, + this.raw, + }); +} + +class NotificationChannel { + final String name; + final String icon; + final String url; + + NotificationChannel({ + required this.name, + required this.icon, + required this.url, + }); +} + +class NotificationMeta { + final String type; + + NotificationMeta({ + required this.type, + }); +} + +class NotificationPayloadMeta { + final String type; + final String domain; + final String data; + + NotificationPayloadMeta({ + required this.type, + required this.domain, + required this.data, + }); +} + +class NotificationMessage { + final NotificationContent notification; + final NotificationPayload? payload; + + NotificationMessage({ + required this.notification, + this.payload, + }); +} + +class NotificationContent { + final String title; + final String body; + + NotificationContent({ + required this.title, + required this.body, + }); +} + +class NotificationPayload { + final String? title; + final String? body; + final String? cta; + final String? embed; + final NotificationPayloadMeta? meta; + + NotificationPayload({ + this.title, + this.body, + this.cta, + this.embed, + this.meta, + }); +} + +class NotificationConfig { + final int? expiry; + final bool? silent; + final bool? hidden; + + NotificationConfig({ + this.expiry, + this.silent, + this.hidden, + }); +} + +class NotificationAdvanced { + final String? chatid; + + NotificationAdvanced({ + this.chatid, + }); +} + +class NotificationRawData { + final String verificationProof; + + NotificationRawData({ + required this.verificationProof, + }); +} + +class NotificationEventType { + static const INBOX = 'notification.inbox'; + static const SPAM = 'notification.spam'; +} diff --git a/lib/src/pushstream/src/push_stream.dart b/lib/src/pushstream/src/push_stream.dart new file mode 100644 index 0000000..c595a24 --- /dev/null +++ b/lib/src/pushstream/src/push_stream.dart @@ -0,0 +1,321 @@ +import 'package:events_emitter/events_emitter.dart'; +import 'package:socket_io_client/socket_io_client.dart' as io; + +import '../../../push_restapi_dart.dart'; + +class PushStream extends EventEmitter { + io.Socket? pushChatSocket; + io.Socket? pushNotificationSocket; + + late final String _account; + late final bool _raw; + late final PushStreamInitializeOptions _options; + late final List _listen; + late final Signer? _signer; + + late final Chat chatInstance; + PushStream({ + required PushStreamInitializeOptions options, + required String account, + required List listen, + Signer? signer, + String? decryptedPgpPvtKey, + void Function(ProgressHookType)? progressHook, + }) { + _account = account; + _options = options; + _raw = options.raw; + _listen = listen; + _signer = signer; + + chatInstance = Chat( + signer: signer, + account: _account, + decryptedPgpPvtKey: decryptedPgpPvtKey, + progressHook: progressHook, + ); + } + + static Future initialize( + {required String account, + required List listen, + Signer? signer, + String? decryptedPgpPvtKey, + PushStreamInitializeOptions? options, + void Function(ProgressHookType)? progressHook}) async { + final defaultOptions = PushStreamInitializeOptions.defaut(); + + if (listen.isEmpty) { + throw Exception( + 'The listen property must have at least one STREAM type.'); + } + + final settings = options ?? defaultOptions; + final accountToUse = settings.overrideAccount ?? account; + + return PushStream( + account: accountToUse, + listen: listen, + options: settings, + decryptedPgpPvtKey: decryptedPgpPvtKey, + signer: signer, + progressHook: progressHook); + } + + Future connect() async { + final shouldInitializeChatSocket = _listen.isNotEmpty || + _listen.contains(STREAM.CHAT) || + _listen.contains(STREAM.CHAT_OPS); + + final shouldInitializeNotifSocket = _listen.isNotEmpty || + _listen.contains(STREAM.NOTIF) || + _listen.contains(STREAM.NOTIF_OPS); + + bool isChatSocketConnected = false; + bool isNotifSocketConnected = false; + + checkAndEmitConnectEvent() { + if (((shouldInitializeChatSocket && isChatSocketConnected) || + !shouldInitializeChatSocket) && + ((shouldInitializeChatSocket && isNotifSocketConnected) || + !shouldInitializeNotifSocket)) { + emit(STREAM.CONNECT.value); + log('Emitted STREAM.CONNECT'); + } + } + + handleSocketDisconnection(String socketType) async { + if (socketType == 'chat') { + isChatSocketConnected = false; + if (isNotifSocketConnected) { + if (pushNotificationSocket != null && + pushNotificationSocket!.connected) { + pushNotificationSocket!.disconnect(); + } + } else { + // Emit STREAM.DISCONNECT only if the chat socket was already disconnected + emit(STREAM.DISCONNECT.value); + log('Emitted STREAM.DISCONNECT '); + } + } else if (socketType == 'notif') { + isNotifSocketConnected = false; + if (isChatSocketConnected) { + if (pushChatSocket != null && pushChatSocket!.connected) { + pushChatSocket!.disconnect(); + } + } else { + // Emit STREAM.DISCONNECT only if the chat socket was already disconnected + emit(STREAM.DISCONNECT.value); + log('Emitted STREAM.DISCONNECT'); + } + } + } + + if (shouldInitializeChatSocket) { + if (pushChatSocket == null) { + // If pushNotificationSocket does not exist, create a new socket connection + pushChatSocket = await createSocketConnection( + SocketInputOptions( + user: walletToPCAIP10(_account), + env: _options.env, + socketType: 'chat', + socketOptions: SocketOptions( + autoConnect: _options.connection!.auto, + reconnectionAttempts: _options.connection!.retries, + ), + ), + ); + + if (pushChatSocket == null) { + throw Exception('Push chat socket not connected'); + } + } else if (!pushChatSocket!.connected) { + // If pushChatSocket exists but is not connected, attempt to reconnect + pushChatSocket!.connect(); + } else { + // If pushChatSocket is already connected + log('Push chat socket already connected'); + } + } + + if (shouldInitializeNotifSocket) { + if (pushNotificationSocket == null) { + // If pushNotificationSocket does not exist, create a new socket connection + pushNotificationSocket = await createSocketConnection( + SocketInputOptions( + user: walletToPCAIP10(_account), + env: _options.env, + socketType: 'notification', + socketOptions: SocketOptions( + autoConnect: _options.connection!.auto, + reconnectionAttempts: _options.connection!.retries, + ), + ), + ); + + if (pushNotificationSocket == null) { + throw Exception('Push notification socket not connected'); + } + } else if (!pushNotificationSocket!.connected) { + // If pushNotificationSocket exists but is not connected, attempt to reconnect + log('Attempting to reconnect push notification socket...'); + pushNotificationSocket!.connect(); + // Assuming connect() is the method to re-establish connection + } else { + // If pushNotificationSocket is already connected + log('Push notification socket already connected'); + } + } + + bool shouldEmit(STREAM eventType) { + if (_listen.isEmpty) { + return false; + } + + return _listen.contains(eventType); + } + + if (pushChatSocket != null) { + pushChatSocket!.on(EVENTS.CONNECT, (data) async { + isChatSocketConnected = true; + checkAndEmitConnectEvent(); + log('Chat Socket Connected (ID: ${pushChatSocket?.id}'); + }); + + pushChatSocket!.on(EVENTS.DISCONNECT, (data) async { + await handleSocketDisconnection('chat'); + }); + pushChatSocket!.on(EVENTS.CHAT_GROUPS, (data) async { + try { + final modifiedData = await DataModifier.handleChatGroupEvent( + data: data, includeRaw: _raw); + modifiedData['event'] = + DataModifier.convertToProposedName(modifiedData.event); + DataModifier.handleToField(modifiedData); + if (_shouldEmitChat(data['chatId'])) { + if (data['eventType'] == GroupEventType.joinGroup || + data['eventType'] == GroupEventType.leaveGroup || + data['eventType'] == MessageEventType.request || + data['eventType'] == GroupEventType.remove) { + if (shouldEmit(STREAM.CHAT)) { + emit(STREAM.CHAT.value, modifiedData); + } + } else { + if (shouldEmit(STREAM.CHAT_OPS)) { + emit(STREAM.CHAT_OPS.value, modifiedData); + } + } + } + } catch (error) { + log('Error handling CHAT_GROUPS event: $error\tData: $data'); + } + }); + + pushChatSocket!.on(EVENTS.CHAT_RECEIVED_MESSAGE, (data) async { + try { + if (data.messageCategory == 'Chat' || + data.messageCategory == 'Request') { + // Dont call this if read only mode ? + if (_signer != null) { + data = await chatInstance + .decrypt(messagePayloads: [Message.fromJson(data)]); + data = data[0]; + } + } + + final modifiedData = DataModifier.handleChatEvent(data, _raw); + modifiedData.event = + DataModifier.convertToProposedName(modifiedData.event); + DataModifier.handleToField(modifiedData); + if (_shouldEmitChat(data.chatId)) { + if (shouldEmit(STREAM.CHAT)) { + emit(STREAM.CHAT.value, modifiedData); + } + } + } catch (error) { + log('Error handling CHAT_RECEIVED_MESSAGE event:$error \t Data:$data'); + } + }); + } + + if (pushNotificationSocket != null) { + pushNotificationSocket!.on(EVENTS.CONNECT, (data) async { + isNotifSocketConnected = true; + checkAndEmitConnectEvent(); + log('Notification Socket Connected (ID: ${pushChatSocket?.id}'); + }); + + pushNotificationSocket!.on(EVENTS.DISCONNECT, (data) async { + await handleSocketDisconnection('notif'); + }); + + pushNotificationSocket!.on(EVENTS.USER_FEEDS, (data) async { + try { + final modifiedData = DataModifier.mapToNotificationEvent( + data: data, + notificationEventType: NotificationEventType.INBOX, + origin: _account == data.sender ? 'self' : 'other', + includeRaw: _raw, + ); + + if (_shouldEmitChannel(modifiedData.from)) { + if (shouldEmit(STREAM.NOTIF)) { + emit(STREAM.NOTIF.value, modifiedData); + } + } + } catch (error) { + log('Error handling USER_FEEDS event: $error \tData: $data'); + } + }); + + pushNotificationSocket!.on(EVENTS.USER_SPAM_FEEDS, (data) { + try { + final modifiedData = DataModifier.mapToNotificationEvent( + data: data, + notificationEventType: NotificationEventType.SPAM, + origin: _account == data['sender'] ? 'self' : 'other', + includeRaw: _raw); + modifiedData.origin = + _account == modifiedData.from ? 'self' : 'other'; + if (_shouldEmitChannel(modifiedData.from)) { + if (shouldEmit(STREAM.NOTIF)) { + emit(STREAM.NOTIF.value, modifiedData); + } + } + } catch (error) { + log('Error handling USER_SPAM_FEEDS event: $error \tData: $data'); + } + }); + } + } + + Future disconnect() async { + if (pushChatSocket != null) { + pushChatSocket!.disconnect(); + } + + if (pushNotificationSocket != null) { + pushNotificationSocket!.disconnect(); + } + } + + bool _shouldEmitChat(String dataChatId) { + if (_options.filter?.chats != null || + _options.filter!.chats!.isNotEmpty || + _options.filter!.chats!.contains('*')) { + return true; + } + + return _options.filter!.chats!.contains(dataChatId); + } + + bool _shouldEmitChannel(String dataChannelId) { + if (_options.filter?.channels != null || + _options.filter!.channels!.isNotEmpty || + _options.filter!.channels!.contains('*')) { + return true; + } + + return _options.filter!.channels!.contains(dataChannelId); + } +} diff --git a/lib/src/video/src/video.dart b/lib/src/video/src/video.dart index 51ba040..8a78745 100644 --- a/lib/src/video/src/video.dart +++ b/lib/src/video/src/video.dart @@ -144,7 +144,6 @@ class VideoCallStateNotifier extends ChangeNotifier { // send a notification containing SDP offer // sendVideoCallNotification( - // // TODO: fill this object // { // signer: , // chainId: , diff --git a/pubspec.yaml b/pubspec.yaml index c1402db..619d960 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -31,6 +31,7 @@ dependencies: ethereum_addresses: ^1.0.2 socket_io_client: ^2.0.2 uuid: ^3.0.4 + events_emitter: ^0.5.2 flutter: assets: