dead_drop_service.dart
1 import 'dart:async'; 2 import 'dart:convert'; 3 import 'dart:typed_data'; 4 5 import '../src/rust/api.dart' as rust_api; 6 7 // Re-export generated types for convenience 8 export '../src/rust/api.dart' 9 show 10 IdentityInfo, 11 ContactInfo, 12 PendingContact, 13 MessageInfo, 14 HandshakeHandle, 15 HandshakeResult, 16 ExchangeHandle, 17 ExchangeResultDto, 18 ExchangeComplete, 19 DocumentInfo, 20 SearchResultInfo, 21 CachedOnionAddress, 22 GossipExchangeHandle, 23 GossipExchangeComplete, 24 GroupInfo, 25 GroupMemberInfoDto; 26 27 /// Main service wrapping Rust core functionality via FFI. 28 /// 29 /// This service provides a Dart-friendly interface to the Dead Drop 30 /// cryptographic core implemented in Rust. All cryptographic operations 31 /// happen in Rust for security. 32 /// 33 /// Usage: 34 /// ```dart 35 /// final service = DeadDropService.instance; 36 /// await service.initialize(storagePath, passphrase); 37 /// ``` 38 class DeadDropService { 39 static DeadDropService? _instance; 40 bool _initialized = false; 41 42 /// Stream controller for message queued events 43 final _messageQueuedController = StreamController<Uint8List>.broadcast(); 44 45 /// Stream that emits contact IDs when a new message is queued 46 Stream<Uint8List> get onMessageQueued => _messageQueuedController.stream; 47 48 DeadDropService._(); 49 50 /// Singleton instance 51 static DeadDropService get instance { 52 _instance ??= DeadDropService._(); 53 return _instance!; 54 } 55 56 /// Whether the service has been initialized 57 bool get isInitialized => _initialized; 58 59 // =========================================================================== 60 // INITIALIZATION 61 // =========================================================================== 62 63 /// Initialize the service with storage path and passphrase. 64 /// 65 /// Must be called before any other operations. 66 /// The passphrase is used to encrypt the local database. 67 Future<void> initialize(String storagePath, String passphrase) async { 68 if (_initialized) return; 69 70 rust_api.initialize(storagePath: storagePath, passphrase: passphrase); 71 _initialized = true; 72 } 73 74 // =========================================================================== 75 // IDENTITY 76 // =========================================================================== 77 78 /// Check if an identity exists in storage. 79 Future<bool> hasIdentity() async { 80 _ensureInitialized(); 81 return rust_api.hasIdentity(); 82 } 83 84 /// Create a new identity (key pair generation). 85 /// 86 /// Returns the identity info including public key fingerprint. 87 Future<rust_api.IdentityInfo> createIdentity() async { 88 _ensureInitialized(); 89 return rust_api.createIdentity(); 90 } 91 92 /// Get the current identity. 93 Future<rust_api.IdentityInfo> getIdentity() async { 94 _ensureInitialized(); 95 return rust_api.getIdentity(); 96 } 97 98 // =========================================================================== 99 // QR CODES 100 // =========================================================================== 101 102 /// Generate QR payload for contact exchange. 103 /// 104 /// Returns a base64-encoded string suitable for QR code display. 105 Future<String> generateQrPayload() async { 106 _ensureInitialized(); 107 return rust_api.generateQrPayload(); 108 } 109 110 /// Process a scanned QR code payload. 111 /// 112 /// Returns pending contact info if valid. 113 Future<rust_api.PendingContact> processQrCode(String payload) async { 114 _ensureInitialized(); 115 return rust_api.processQrCode(payload: payload); 116 } 117 118 /// Confirm and save a pending contact. 119 Future<void> confirmContact( 120 rust_api.PendingContact pending, 121 String? nickname, 122 ) async { 123 _ensureInitialized(); 124 rust_api.confirmContact(pending: pending, nickname: nickname); 125 } 126 127 // =========================================================================== 128 // CONTACTS 129 // =========================================================================== 130 131 /// List all contacts. 132 Future<List<rust_api.ContactInfo>> listContacts() async { 133 _ensureInitialized(); 134 return rust_api.listContacts(); 135 } 136 137 /// Get a specific contact by ID. 138 Future<rust_api.ContactInfo> getContact(Uint8List contactId) async { 139 _ensureInitialized(); 140 return rust_api.getContact(contactId: contactId.toList()); 141 } 142 143 /// Delete a contact. 144 Future<void> deleteContact(Uint8List contactId) async { 145 _ensureInitialized(); 146 rust_api.deleteContact(contactId: contactId.toList()); 147 } 148 149 /// Update a contact's nickname. 150 Future<void> updateContactNickname( 151 Uint8List contactId, 152 String? nickname, 153 ) async { 154 _ensureInitialized(); 155 rust_api.updateContactNickname( 156 contactId: contactId.toList(), 157 nickname: nickname, 158 ); 159 } 160 161 /// Block a contact. 162 Future<void> blockContact(Uint8List contactId) async { 163 _ensureInitialized(); 164 rust_api.blockContact(contactId: contactId.toList()); 165 } 166 167 /// Unblock a contact. 168 Future<void> unblockContact(Uint8List contactId) async { 169 _ensureInitialized(); 170 rust_api.unblockContact(contactId: contactId.toList()); 171 } 172 173 // =========================================================================== 174 // MESSAGES 175 // =========================================================================== 176 177 /// Send a text message to a contact. 178 /// 179 /// Returns the message ID. 180 Future<Uint8List> sendTextMessage( 181 Uint8List recipientId, 182 String text, { 183 int ttlDays = 30, 184 }) async { 185 _ensureInitialized(); 186 final messageId = await rust_api.sendMessage( 187 recipientId: recipientId.toList(), 188 content: utf8.encode(text), 189 contentType: ContentType.text, 190 ttlDays: ttlDays, 191 ); 192 // Notify listeners that a new message is queued for this contact 193 _messageQueuedController.add(recipientId); 194 return Uint8List.fromList(messageId); 195 } 196 197 /// Send a document/file to a contact. 198 /// 199 /// For small files (<= 30KB), sends as a single message. 200 /// For large files, automatically chunks the file. 201 /// Returns the document ID. 202 Future<Uint8List> sendDocument( 203 Uint8List recipientId, 204 Uint8List content, 205 String filename, { 206 int ttlDays = 30, 207 }) async { 208 _ensureInitialized(); 209 final documentId = await rust_api.sendDocument( 210 recipientId: recipientId.toList(), 211 content: content.toList(), 212 filename: filename, 213 ttlDays: ttlDays, 214 ); 215 // Notify listeners that a new message is queued for this contact 216 _messageQueuedController.add(recipientId); 217 return Uint8List.fromList(documentId); 218 } 219 220 /// Reassemble a chunked document from received chunks. 221 /// 222 /// Returns document info including content if complete. 223 Future<rust_api.DocumentInfo> reassembleDocument(Uint8List documentId) async { 224 _ensureInitialized(); 225 return rust_api.reassembleDocument(documentId: documentId.toList()); 226 } 227 228 /// Check if all chunks have been received for a document. 229 bool isDocumentComplete(Uint8List documentId) { 230 _ensureInitialized(); 231 return rust_api.isDocumentComplete(documentId: documentId.toList()); 232 } 233 234 /// Get document info (filename and size) without reassembling. 235 /// Returns (filename, size) tuple or null if not found. 236 (String, int)? getDocumentInfo(Uint8List documentId) { 237 _ensureInitialized(); 238 final info = rust_api.getDocumentInfo(documentId: documentId.toList()); 239 if (info == null) return null; 240 // info.$2 is BigInt for u64, convert to int 241 return (info.$1, info.$2.toInt()); 242 } 243 244 /// Get send progress for a chunked document. 245 /// Returns (delivered, total) or null if not a chunked send. 246 (int, int)? getSendProgress(Uint8List documentId) { 247 _ensureInitialized(); 248 return rust_api.getSendProgress(documentId: documentId.toList()); 249 } 250 251 /// Get receive progress for a chunked document. 252 /// Returns (received, total) or null if not found. 253 (int, int)? getReceiveProgress(Uint8List documentId) { 254 _ensureInitialized(); 255 return rust_api.getReceiveProgress(documentId: documentId.toList()); 256 } 257 258 /// Search messages within a conversation. 259 List<rust_api.SearchResultInfo> searchConversation( 260 Uint8List contactId, 261 String query, { 262 int limit = 50, 263 }) { 264 _ensureInitialized(); 265 return rust_api.searchConversation( 266 contactId: contactId.toList(), 267 query: query, 268 limit: limit, 269 ); 270 } 271 272 /// Search all messages across all conversations. 273 List<rust_api.SearchResultInfo> searchAllMessages( 274 String query, { 275 int limit = 50, 276 }) { 277 _ensureInitialized(); 278 return rust_api.searchAllMessages(query: query, limit: limit); 279 } 280 281 /// Get conversation with a contact. 282 Future<List<rust_api.MessageInfo>> getConversation( 283 Uint8List contactId, 284 ) async { 285 _ensureInitialized(); 286 return rust_api.getConversation(contactId: contactId.toList()); 287 } 288 289 /// Get outbound message queue for a contact. 290 Future<List<rust_api.MessageInfo>> getOutboundQueue( 291 Uint8List contactId, 292 ) async { 293 _ensureInitialized(); 294 return rust_api.getOutboundQueue(contactId: contactId.toList()); 295 } 296 297 /// Mark a message as read. 298 void markMessageRead(Uint8List messageId) { 299 _ensureInitialized(); 300 rust_api.markRead(messageId: messageId.toList()); 301 } 302 303 /// Delete a message. 304 Future<void> deleteMessage(Uint8List messageId) async { 305 _ensureInitialized(); 306 rust_api.deleteMessage(messageId: messageId.toList()); 307 } 308 309 /// Clear all messages for a contact. 310 Future<int> clearConversation(Uint8List contactId) async { 311 _ensureInitialized(); 312 return rust_api.clearConversation(contactId: contactId.toList()); 313 } 314 315 /// Get total unread message count. 316 Future<int> getUnreadCount() async { 317 _ensureInitialized(); 318 return rust_api.getUnreadCount(); 319 } 320 321 // =========================================================================== 322 // GROUPS 323 // =========================================================================== 324 325 /// Create a new group with the given name and members. 326 /// Returns the group ID (16 bytes). 327 Future<Uint8List> createGroup(String name, List<Uint8List> memberIds) async { 328 _ensureInitialized(); 329 final groupId = await rust_api.createGroup( 330 name: name, 331 memberIds: memberIds, 332 ); 333 return Uint8List.fromList(groupId); 334 } 335 336 /// List all groups. 337 List<rust_api.GroupInfo> listGroups() { 338 _ensureInitialized(); 339 return rust_api.listGroups(); 340 } 341 342 /// Get members of a group. 343 List<rust_api.GroupMemberInfoDto> getGroupMembers(Uint8List groupId) { 344 _ensureInitialized(); 345 return rust_api.getGroupMembers(groupId: groupId.toList()); 346 } 347 348 /// Send a message to all group members (fan-out). 349 /// Returns the message ID. 350 Future<Uint8List> sendGroupMessage( 351 Uint8List groupId, 352 List<int> content, 353 int contentType, { 354 String? filename, 355 }) async { 356 _ensureInitialized(); 357 final messageId = await rust_api.sendGroupMessage( 358 groupId: groupId.toList(), 359 content: content, 360 contentType: contentType, 361 filename: filename, 362 ); 363 return Uint8List.fromList(messageId); 364 } 365 366 /// Send a document to a group, automatically chunking if larger than 30KB. 367 /// Returns the document ID. 368 Future<Uint8List> sendGroupDocument( 369 Uint8List groupId, 370 Uint8List content, 371 String filename, 372 ) async { 373 _ensureInitialized(); 374 final documentId = await rust_api.sendGroupDocument( 375 groupId: groupId.toList(), 376 content: content.toList(), 377 filename: filename, 378 ); 379 return Uint8List.fromList(documentId); 380 } 381 382 /// Accept a group invite. 383 void acceptGroupInvite(Uint8List groupId) { 384 _ensureInitialized(); 385 rust_api.acceptGroupInvite(groupId: groupId.toList()); 386 } 387 388 /// Decline a group invite (notify members, then delete locally). 389 Future<void> declineGroupInvite(Uint8List groupId) async { 390 _ensureInitialized(); 391 await rust_api.declineGroupInvite(groupId: groupId.toList()); 392 } 393 394 /// Get conversation messages for a group. 395 List<rust_api.MessageInfo> getGroupConversation(Uint8List groupId) { 396 _ensureInitialized(); 397 return rust_api.getGroupConversation(groupId: groupId.toList()); 398 } 399 400 /// Add a member to a group. 401 Future<void> addGroupMember(Uint8List groupId, Uint8List contactId) async { 402 _ensureInitialized(); 403 await rust_api.addGroupMember( 404 groupId: groupId.toList(), 405 contactId: contactId.toList(), 406 ); 407 } 408 409 /// Remove a member from a group. 410 Future<void> removeGroupMember(Uint8List groupId, Uint8List contactId) async { 411 _ensureInitialized(); 412 await rust_api.removeGroupMember( 413 groupId: groupId.toList(), 414 contactId: contactId.toList(), 415 ); 416 } 417 418 /// Leave a group. 419 Future<void> leaveGroup(Uint8List groupId) async { 420 _ensureInitialized(); 421 await rust_api.leaveGroup(groupId: groupId.toList()); 422 } 423 424 /// Rename a group. 425 Future<void> renameGroup(Uint8List groupId, String newName) async { 426 _ensureInitialized(); 427 await rust_api.renameGroup( 428 groupId: groupId.toList(), 429 newName: newName, 430 ); 431 } 432 433 /// Set group muted status. 434 void setGroupMuted(Uint8List groupId, bool isMuted) { 435 _ensureInitialized(); 436 rust_api.setGroupMuted(groupId: groupId.toList(), isMuted: isMuted); 437 } 438 439 /// Set group disappearing message duration. 440 void setGroupDisappearing(Uint8List groupId, int duration) { 441 _ensureInitialized(); 442 rust_api.setGroupDisappearing( 443 groupId: groupId.toList(), 444 duration: BigInt.from(duration), 445 ); 446 } 447 448 // =========================================================================== 449 // ROTATING IDS (BLE Discovery) 450 // =========================================================================== 451 452 /// Get my current rotating IDs for BLE advertising. 453 /// 454 /// Returns a list of 16-byte rotating IDs, one per contact. 455 Future<List<Uint8List>> getMyRotatingIds() async { 456 _ensureInitialized(); 457 return rust_api.getMyRotatingIds(); 458 } 459 460 /// Check if a scanned rotating ID matches any contact. 461 /// 462 /// Returns the contact ID if found, null otherwise. 463 Future<Uint8List?> checkRotatingId(Uint8List scannedId) async { 464 _ensureInitialized(); 465 return rust_api.checkRotatingId(scannedId: scannedId.toList()); 466 } 467 468 // =========================================================================== 469 // HANDSHAKE (Noise Protocol) 470 // =========================================================================== 471 472 /// Create a handshake initiator for a contact. 473 Future<rust_api.HandshakeHandle> createHandshakeInitiator( 474 Uint8List contactId, 475 ) async { 476 _ensureInitialized(); 477 return rust_api.createHandshakeInitiator(contactId: contactId.toList()); 478 } 479 480 /// Create an anonymous handshake initiator for gossip-only connections. 481 Future<rust_api.HandshakeHandle> createHandshakeInitiatorAnonymous() async { 482 _ensureInitialized(); 483 return rust_api.createHandshakeInitiatorAnonymous(); 484 } 485 486 /// Create a handshake responder. 487 Future<rust_api.HandshakeHandle> createHandshakeResponder() async { 488 _ensureInitialized(); 489 return rust_api.createHandshakeResponder(); 490 } 491 492 /// Generate handshake message. 493 Future<Uint8List> handshakeGenerate(rust_api.HandshakeHandle handle) async { 494 _ensureInitialized(); 495 return rust_api.handshakeGenerate(handle: handle); 496 } 497 498 /// Process received handshake message. 499 Future<rust_api.HandshakeResult> handshakeProcess( 500 rust_api.HandshakeHandle handle, 501 Uint8List data, 502 ) async { 503 _ensureInitialized(); 504 return rust_api.handshakeProcess(handle: handle, data: data.toList()); 505 } 506 507 /// Cancel a handshake. 508 Future<void> handshakeCancel(rust_api.HandshakeHandle handle) async { 509 _ensureInitialized(); 510 rust_api.handshakeCancel(handle: handle); 511 } 512 513 /// Finalize a completed handshake directly into a gossip exchange. 514 /// All BLE connections use this path (gossip-only architecture). 515 rust_api.GossipExchangeHandle handshakeFinalizeForGossip( 516 rust_api.HandshakeHandle handle, 517 ) { 518 _ensureInitialized(); 519 return rust_api.handshakeFinalizeForGossip(handle: handle); 520 } 521 522 // =========================================================================== 523 // BLE GOSSIP EXCHANGE 524 // =========================================================================== 525 526 /// Get the next gossip data to send over BLE. 527 Uint8List? gossipExchangeSend(rust_api.GossipExchangeHandle handle) { 528 _ensureInitialized(); 529 return rust_api.gossipExchangeSend(handle: handle); 530 } 531 532 /// Process encrypted gossip data received from BLE. 533 /// Returns true if the gossip exchange is now complete. 534 bool gossipExchangeReceive( 535 rust_api.GossipExchangeHandle handle, 536 Uint8List data, 537 ) { 538 _ensureInitialized(); 539 return rust_api.gossipExchangeReceive( 540 handle: handle, 541 data: data.toList(), 542 ); 543 } 544 545 /// Finalize a gossip exchange and get results. 546 rust_api.GossipExchangeComplete gossipExchangeFinalize( 547 rust_api.GossipExchangeHandle handle, 548 ) { 549 _ensureInitialized(); 550 return rust_api.gossipExchangeFinalize(handle: handle); 551 } 552 553 /// Cancel a gossip exchange and clean up resources. 554 void gossipExchangeCancel(rust_api.GossipExchangeHandle handle) { 555 _ensureInitialized(); 556 rust_api.gossipExchangeCancel(handle: handle); 557 } 558 559 /// Finalize a gossip exchange but preserve the transport for reuse. 560 rust_api.GossipFinalizeKeepTransportResult gossipExchangeFinalizeKeepTransport( 561 rust_api.GossipExchangeHandle handle, 562 ) { 563 _ensureInitialized(); 564 return rust_api.gossipExchangeFinalizeKeepTransport(handle: handle); 565 } 566 567 /// Create a new gossip exchange round from a stored transport. 568 rust_api.GossipExchangeHandle gossipStartNewRound(BigInt transportHandle) { 569 _ensureInitialized(); 570 return rust_api.gossipStartNewRound(transportHandle: transportHandle); 571 } 572 573 /// Release a stored gossip transport (cleanup on disconnect). 574 void gossipTransportRelease(BigInt transportHandle) { 575 _ensureInitialized(); 576 rust_api.gossipTransportRelease(transportHandle: transportHandle); 577 } 578 579 // =========================================================================== 580 // DHT MESSAGING 581 // =========================================================================== 582 583 /// Publish queued messages to DHT for a contact. 584 /// 585 /// Returns the number of messages published. 586 Future<int> publishToDht(Uint8List contactId) async { 587 _ensureInitialized(); 588 final result = await rust_api.publishMessagesToDht(contactId: contactId.toList()); 589 return result.publishedCount; 590 } 591 592 /// Fetch messages from DHT for our public key. 593 /// 594 /// Returns messages and contacts with reaction updates. 595 Future<rust_api.DhtFetchResult> fetchFromDht() async { 596 _ensureInitialized(); 597 return await rust_api.fetchMessagesFromDht(); 598 } 599 600 /// Acknowledge DHT messages (mark as received). 601 Future<void> acknowledgeDhtMessages(List<Uint8List> messageIds) async { 602 _ensureInitialized(); 603 rust_api.acknowledgeDhtMessages(messageIds: messageIds); 604 } 605 606 // =========================================================================== 607 // TOR P2P RENDEZVOUS (DHT) 608 // =========================================================================== 609 610 /// Publish our .onion address to the DHT for peer discovery. 611 Future<void> publishOnionAddress(String onionAddress) async { 612 _ensureInitialized(); 613 await rust_api.publishOnionAddress(onionAddress: onionAddress); 614 } 615 616 /// Fetch a contact's .onion address from the DHT. 617 /// Returns null if not found or stale (>1 hour old). 618 Future<String?> fetchContactOnionAddress(Uint8List contactId) async { 619 _ensureInitialized(); 620 return await rust_api.fetchContactOnionAddress( 621 contactId: contactId.toList()); 622 } 623 624 /// Get all cached onion addresses for contacts. 625 /// Used on startup to connect instantly without waiting for DHT rendezvous. 626 Future<List<rust_api.CachedOnionAddress>> getCachedOnionAddresses() async { 627 _ensureInitialized(); 628 return rust_api.getCachedOnionAddresses(); 629 } 630 631 // =========================================================================== 632 // TOR P2P MESSAGING 633 // =========================================================================== 634 635 /// Get queued encrypted messages ready for P2P delivery to a contact. 636 Future<List<rust_api.EncryptedPayload>> getQueuedEncrypted( 637 Uint8List contactId) async { 638 _ensureInitialized(); 639 return rust_api.getQueuedEncrypted(contactId: contactId.toList()); 640 } 641 642 /// Re-queue any Failed messages for a contact back to Queued state. 643 Future<int> requeueFailedMessages(Uint8List contactId) async { 644 _ensureInitialized(); 645 return rust_api.requeueFailedMessages(contactId: contactId.toList()); 646 } 647 648 /// Queue an outbound message for BLE gossip mesh relay. 649 /// Returns true if successfully queued, false if not applicable. 650 bool queueForGossipRelay(Uint8List messageId) { 651 _ensureInitialized(); 652 return rust_api.queueForGossipRelay(messageId: messageId.toList()); 653 } 654 655 /// Mark messages as delivered via Tor P2P transport. 656 Future<void> markDeliveredViaTor(List<Uint8List> messageIds) async { 657 _ensureInitialized(); 658 await rust_api.markDeliveredViaTor(messageIds: messageIds); 659 } 660 661 /// Process an encrypted message received via Tor P2P. 662 /// Returns the message info if successfully processed (null for reactions/chunks). 663 Future<rust_api.MessageInfo?> processTorMessage( 664 Uint8List senderIdentityKey, Uint8List payload) async { 665 _ensureInitialized(); 666 return rust_api.processTorMessage( 667 senderIdentityKey: senderIdentityKey.toList(), 668 payload: payload.toList(), 669 ); 670 } 671 672 /// Get a contact's identity public key (ed25519, 32 bytes). 673 Uint8List getContactIdentityKey(Uint8List contactId) { 674 _ensureInitialized(); 675 return rust_api.getContactIdentityKey(contactId: contactId.toList()); 676 } 677 678 /// Look up a contact's ID by their identity public key (ed25519). 679 Future<Uint8List?> getContactIdByIdentityKey(Uint8List identityKey) async { 680 _ensureInitialized(); 681 final result = await rust_api.getContactIdByIdentityKey( 682 identityKey: identityKey.toList(), 683 ); 684 return result != null ? Uint8List.fromList(result) : null; 685 } 686 687 // =========================================================================== 688 // IROH P2P TRANSPORT 689 // =========================================================================== 690 691 /// Get the current state of the iroh transport. 692 rust_api.IrohStateDto getIrohState() { 693 _ensureInitialized(); 694 return rust_api.getIrohState(); 695 } 696 697 /// Start the iroh P2P transport. 698 Future<void> startIroh() async { 699 _ensureInitialized(); 700 await rust_api.startIroh(); 701 } 702 703 /// Stop the iroh P2P transport. 704 Future<void> stopIroh() async { 705 _ensureInitialized(); 706 await rust_api.stopIroh(); 707 } 708 709 /// Publish our iroh EndpointId to the DHT for peer discovery. 710 Future<void> publishIrohEndpointId() async { 711 _ensureInitialized(); 712 await rust_api.publishIrohEndpointId(); 713 } 714 715 /// Fetch a contact's iroh EndpointId from the DHT. 716 Future<String?> fetchContactIrohEndpointId(Uint8List contactId) async { 717 _ensureInitialized(); 718 return await rust_api.fetchContactIrohEndpointId( 719 contactId: contactId.toList()); 720 } 721 722 /// Send an encrypted payload to a peer via iroh. 723 Future<void> sendViaIroh(String endpointId, Uint8List payload) async { 724 _ensureInitialized(); 725 await rust_api.sendViaIroh( 726 endpointId: endpointId, payload: payload.toList()); 727 } 728 729 /// Process an encrypted message received via iroh P2P. 730 Future<rust_api.MessageInfo?> processIrohMessage( 731 Uint8List senderIdentityKey, Uint8List payload) async { 732 _ensureInitialized(); 733 return rust_api.processIrohMessage( 734 senderIdentityKey: senderIdentityKey.toList(), 735 payload: payload.toList(), 736 ); 737 } 738 739 /// Process an iroh message when the sender's endpoint ID is unknown. 740 Future<rust_api.MessageInfo?> processIrohMessageUnknownSender({ 741 required Uint8List payload, 742 }) async { 743 _ensureInitialized(); 744 return rust_api.processIrohMessageUnknownSender( 745 payload: payload.toList(), 746 ); 747 } 748 749 /// Poll for incoming iroh messages (non-blocking). 750 Future<rust_api.IrohIncomingMessage?> pollIrohIncoming() async { 751 _ensureInitialized(); 752 return rust_api.pollIrohIncoming(); 753 } 754 755 /// Get all cached iroh endpoint IDs for contacts. 756 Future<List<rust_api.CachedIrohEndpoint>> getCachedIrohEndpointIds() async { 757 _ensureInitialized(); 758 return rust_api.getCachedIrohEndpointIds(); 759 } 760 761 /// Manually set a contact's iroh endpoint ID (bypasses DHT). 762 void setContactIrohEndpoint(Uint8List contactId, String endpointId) { 763 _ensureInitialized(); 764 rust_api.setContactIrohEndpoint( 765 contactId: contactId.toList(), 766 endpointId: endpointId, 767 ); 768 } 769 770 // =========================================================================== 771 // MAINTENANCE 772 // =========================================================================== 773 774 /// Run maintenance tasks (cleanup expired messages, etc.) 775 Future<void> runMaintenance() async { 776 _ensureInitialized(); 777 rust_api.runMaintenance(); 778 } 779 780 /// Get database statistics. 781 Future<String> getStatistics() async { 782 _ensureInitialized(); 783 return rust_api.getStatistics(); 784 } 785 786 // =========================================================================== 787 // PRIVATE HELPERS 788 // =========================================================================== 789 790 void _ensureInitialized() { 791 if (!_initialized) { 792 throw StateError( 793 'DeadDropService not initialized. Call initialize() first.', 794 ); 795 } 796 } 797 } 798 799 // ============================================================================= 800 // HELPER EXTENSIONS 801 // ============================================================================= 802 803 /// Extension to get display name from ContactInfo 804 extension ContactInfoDisplay on rust_api.ContactInfo { 805 /// Display name (nickname or fingerprint) 806 String get displayName => nickname ?? fingerprint; 807 } 808 809 /// Contact state constants 810 class ContactState { 811 static const int pending = 0; 812 static const int active = 1; 813 static const int blocked = 2; 814 static const int revoked = 3; 815 } 816 817 /// Message content type constants (matches Rust ContentType enum) 818 class ContentType { 819 static const int text = 0; 820 static const int document = 1; 821 static const int reaction = 2; 822 static const int documentChunk = 3; 823 static const int deliveryReceipt = 4; 824 static const int groupManagement = 5; 825 } 826 827 /// Message state constants 828 class MessageState { 829 static const int pending = 0; 830 static const int sent = 1; 831 static const int delivered = 2; 832 static const int failed = 3; 833 }