[READ ONLY MIRROR] Open Source TikTok alternative built on AT Protocol github.com/sprksocial/client
flutter atproto video dart
10
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix(auth): remove incorrect did and unify record creations

+222 -344
+146 -221
lib/src/core/network/atproto/data/repositories/feed_repository_impl.dart
··· 687 687 @override 688 688 Future<RepoStrongRef> likePost(String postCid, AtUri postUri) async { 689 689 _logger.d('Liking post with String: $postCid, URI: $postUri'); 690 - return _client.executeWithRetry(() async { 691 - if (!_client.authRepository.isAuthenticated) { 692 - _logger.w('Not authenticated'); 693 - throw Exception('Not authenticated'); 694 - } 695 690 696 - final atproto = _client.authRepository.atproto; 697 - if (atproto == null) { 698 - _logger.e('AtProto not initialized'); 699 - throw Exception('AtProto not initialized'); 700 - } 691 + // Determine if this is a Bluesky post or Spark post 692 + final isBskyPost = postUri.collection.toString().startsWith('app.bsky.feed.post'); 693 + final likeType = isBskyPost ? 'app.bsky.feed.like' : 'so.sprk.feed.like'; 701 694 702 - // Determine if this is a Bluesky post or Spark post 703 - final isBskyPost = postUri.collection.toString().startsWith('app.bsky.feed.post'); 704 - final likeType = isBskyPost ? 'app.bsky.feed.like' : 'so.sprk.feed.like'; 705 - final likeCollection = likeType; 706 - 707 - _logger.d('Post type: ${isBskyPost ? 'Bluesky' : 'Spark'}, using collection: $likeType'); 708 - 709 - final likeRecord = { 710 - r'$type': likeType, 711 - 'subject': {'cid': postCid, 'uri': postUri.toString()}, 712 - 'createdAt': DateTime.now().toUtc().toIso8601String(), 713 - }; 695 + _logger.d('Post type: ${isBskyPost ? 'Bluesky' : 'Spark'}, using collection: $likeType'); 714 696 715 - final result = await atproto.repo.createRecord(repo: _client.sprkDid, collection: likeCollection, record: likeRecord); 697 + final likeRecord = { 698 + r'$type': likeType, 699 + 'subject': {'cid': postCid, 'uri': postUri.toString()}, 700 + 'createdAt': DateTime.now().toUtc().toIso8601String(), 701 + }; 716 702 717 - _logger.i('Post liked successfully: ${result.data.uri}'); 703 + final result = await _client.repo.createRecord(collection: likeType, record: likeRecord); 704 + _logger.i('Post liked successfully: ${result.uri}'); 718 705 719 - return result.data as RepoStrongRef; 720 - }); 706 + return result; 721 707 } 722 708 723 709 @override 724 710 Future<void> unlikePost(AtUri likeUri) async { 725 711 _logger.d('Unliking post with like URI: $likeUri'); 726 - return _client.executeWithRetry(() async { 727 - if (!_client.authRepository.isAuthenticated) { 728 - _logger.w('Not authenticated'); 729 - throw Exception('Not authenticated'); 730 - } 731 - 732 - final atproto = _client.authRepository.atproto; 733 - if (atproto == null) { 734 - _logger.e('AtProto not initialized'); 735 - throw Exception('AtProto not initialized'); 736 - } 737 - 738 - await atproto.repo.deleteRecord(repo: _client.sprkDid, collection: likeUri.collection.toString(), rkey: likeUri.rkey); 739 - _logger.i('Post unliked successfully'); 740 - }); 712 + await _client.repo.deleteRecord(uri: likeUri, skipBskyCrosspostCleanup: true); 713 + _logger.i('Post unliked successfully'); 741 714 } 742 715 743 716 @override ··· 752 725 }) async { 753 726 _logger.d('Posting comment to parent: $parentUri'); 754 727 755 - return _client.executeWithRetry(() async { 756 - if (!_client.authRepository.isAuthenticated) { 757 - _logger.w('Not authenticated'); 758 - throw Exception('Not authenticated'); 759 - } 728 + if (!_client.authRepository.isAuthenticated) { 729 + _logger.w('Not authenticated'); 730 + throw Exception('Not authenticated'); 731 + } 760 732 761 - switch (_client.authRepository.atproto) { 762 - case null: 763 - _logger.e('AtProto not initialized'); 764 - throw Exception('AtProto not initialized'); 765 - case final atproto: 766 - // Use parent as root if not provided 767 - final effectiveRootCid = rootCid ?? parentCid; 768 - final effectiveRootUri = rootUri ?? parentUri; 733 + if (_client.authRepository.atproto == null) { 734 + _logger.e('AtProto not initialized'); 735 + throw Exception('AtProto not initialized'); 736 + } 769 737 770 - // Upload image if provided (replies only support single image) 771 - Map<String, dynamic>? mediaJson; 772 - if (imageFiles case final List<XFile> files when files.isNotEmpty) { 773 - if (files.length > 1) { 774 - _logger.w('Replies only support single image, using first image only'); 775 - } 776 - final uploadedImageMaps = await uploadImages(imageFiles: [files.first], altTexts: altTexts); 777 - final firstImage = uploadedImageMaps.first; 778 - mediaJson = Media.image(image: firstImage.image, alt: firstImage.alt).toJson(); 779 - } 738 + // Use parent as root if not provided 739 + final effectiveRootCid = rootCid ?? parentCid; 740 + final effectiveRootUri = rootUri ?? parentUri; 780 741 781 - // Create the correct record JSON depending on the target platform. 782 - final isSprk = parentUri.toString().contains('sprk'); 742 + // Upload image if provided (replies only support single image) 743 + Map<String, dynamic>? mediaJson; 744 + if (imageFiles case final List<XFile> files when files.isNotEmpty) { 745 + if (files.length > 1) { 746 + _logger.w('Replies only support single image, using first image only'); 747 + } 748 + final uploadedImageMaps = await uploadImages(imageFiles: [files.first], altTexts: altTexts); 749 + final firstImage = uploadedImageMaps.first; 750 + mediaJson = Media.image(image: firstImage.image, alt: firstImage.alt).toJson(); 751 + } 783 752 784 - final Map<String, dynamic> recordJson; 785 - final NSID collection; 753 + // Create the correct record JSON depending on the target platform. 754 + final isSprk = parentUri.toString().contains('sprk'); 786 755 787 - if (isSprk) { 788 - // Sprk comment 789 - final media = mediaJson != null ? Media.fromJson(mediaJson) : null; 756 + final Map<String, dynamic> recordJson; 757 + final NSID collection; 790 758 791 - // Validate that videos are not allowed in replies 792 - if (media != null && (media is MediaVideo || media is MediaBskyVideo)) { 793 - throw Exception('Videos are not allowed in replies'); 794 - } 759 + if (isSprk) { 760 + // Sprk comment 761 + final media = mediaJson != null ? Media.fromJson(mediaJson) : null; 795 762 796 - final sprkRecord = ReplyRecord( 797 - caption: CaptionRef(text: text, facets: []), 798 - reply: RecordReplyRef( 799 - root: RepoStrongRef(uri: effectiveRootUri, cid: effectiveRootCid), 800 - parent: RepoStrongRef(uri: parentUri, cid: parentCid), 801 - ), 802 - createdAt: DateTime.now().toUtc(), 803 - media: media, 804 - ); 805 - recordJson = sprkRecord.toJson(); 806 - collection = NSID.parse('so.sprk.feed.reply'); 807 - } else { 808 - // Bluesky comment - use adapter to create Bluesky-specific models 809 - // Validate that videos are not allowed in replies before conversion 810 - if (mediaJson != null) { 811 - final media = Media.fromJson(mediaJson); 812 - if (media is MediaVideo || media is MediaBskyVideo) { 813 - _logger.e('Videos are not allowed in replies'); 814 - throw Exception('Videos are not allowed in replies'); 815 - } 816 - } 763 + // Validate that videos are not allowed in replies 764 + if (media != null && (media is MediaVideo || media is MediaBskyVideo)) { 765 + throw Exception('Videos are not allowed in replies'); 766 + } 817 767 818 - final bskyMedia = mediaJson != null ? bskyFeedAdapter.convertJsonToBskyEmbed(mediaJson) : null; 768 + final sprkRecord = ReplyRecord( 769 + caption: CaptionRef(text: text, facets: []), 770 + reply: RecordReplyRef( 771 + root: RepoStrongRef(uri: effectiveRootUri, cid: effectiveRootCid), 772 + parent: RepoStrongRef(uri: parentUri, cid: parentCid), 773 + ), 774 + createdAt: DateTime.now().toUtc(), 775 + media: media, 776 + ); 777 + recordJson = sprkRecord.toJson(); 778 + collection = NSID.parse('so.sprk.feed.reply'); 779 + } else { 780 + // Bluesky comment - use adapter to create Bluesky-specific models 781 + // Validate that videos are not allowed in replies before conversion 782 + if (mediaJson != null) { 783 + final media = Media.fromJson(mediaJson); 784 + if (media is MediaVideo || media is MediaBskyVideo) { 785 + _logger.e('Videos are not allowed in replies'); 786 + throw Exception('Videos are not allowed in replies'); 787 + } 788 + } 819 789 820 - final bskyRecord = bskyFeedAdapter.createCommentRecord( 821 - text: text, 822 - createdAt: DateTime.now().toUtc(), 823 - reply: RecordReplyRef( 824 - root: RepoStrongRef(uri: effectiveRootUri, cid: effectiveRootCid), 825 - parent: RepoStrongRef(uri: parentUri, cid: parentCid), 826 - ), 827 - embed: bskyMedia, 828 - ); 829 - recordJson = bskyRecord.toJson(); 830 - collection = NSID.parse('app.bsky.feed.post'); 831 - } 790 + final bskyMedia = mediaJson != null ? bskyFeedAdapter.convertJsonToBskyEmbed(mediaJson) : null; 791 + 792 + final bskyRecord = bskyFeedAdapter.createCommentRecord( 793 + text: text, 794 + createdAt: DateTime.now().toUtc(), 795 + reply: RecordReplyRef( 796 + root: RepoStrongRef(uri: effectiveRootUri, cid: effectiveRootCid), 797 + parent: RepoStrongRef(uri: parentUri, cid: parentCid), 798 + ), 799 + embed: bskyMedia, 800 + ); 801 + recordJson = bskyRecord.toJson(); 802 + collection = NSID.parse('app.bsky.feed.post'); 803 + } 832 804 833 - final result = await atproto.repo.createRecord( 834 - repo: _client.sprkDid, 835 - collection: collection.toString(), 836 - record: recordJson, 837 - ); 805 + final result = await _client.repo.createRecord( 806 + collection: collection.toString(), 807 + record: recordJson, 808 + ); 838 809 839 - _logger.i('Comment posted successfully: ${result.data.uri}'); 810 + _logger.i('Comment posted successfully: ${result.uri}'); 840 811 841 - return result.data as RepoStrongRef; 842 - } 843 - }); 812 + return result; 844 813 } 845 814 846 815 @override ··· 850 819 Map<String, String> altTexts, { 851 820 bool crosspostToBsky = false, 852 821 }) async { 853 - switch (imageFiles) { 854 - case final List<XFile> files when files.isEmpty: 855 - _logger.e('No images provided for image post'); 856 - throw ArgumentError('At least one image is required for an image post.'); 857 - default: 858 - return _client.executeWithRetry(() async { 859 - if (!_client.authRepository.isAuthenticated) { 860 - _logger.w('Not authenticated'); 861 - throw Exception('Not authenticated'); 862 - } 822 + if (imageFiles.isEmpty) { 823 + _logger.e('No images provided for image post'); 824 + throw ArgumentError('At least one image is required for an image post.'); 825 + } 863 826 864 - if (_client.authRepository.atproto case final atproto?) { 865 - final uploadedImageMaps = await uploadImages(imageFiles: imageFiles, altTexts: altTexts); 827 + if (!_client.authRepository.isAuthenticated) { 828 + _logger.w('Not authenticated'); 829 + throw Exception('Not authenticated'); 830 + } 866 831 867 - // Create Sprk post first 868 - final record = PostRecord( 869 - caption: CaptionRef(text: text, facets: []), 870 - media: Media.images(images: uploadedImageMaps), 871 - createdAt: DateTime.now().toUtc(), 872 - ); 832 + if (_client.authRepository.atproto == null) { 833 + _logger.e('AtProto not initialized'); 834 + throw Exception('AtProto not initialized'); 835 + } 836 + 837 + final uploadedImageMaps = await uploadImages(imageFiles: imageFiles, altTexts: altTexts); 873 838 874 - final result = await atproto.repo.createRecord( 875 - repo: _client.sprkDid, 876 - collection: NSID.parse('so.sprk.feed.post').toString(), 877 - record: record.toJson(), 878 - ); 839 + // Create Sprk post 840 + final record = PostRecord( 841 + caption: CaptionRef(text: text, facets: []), 842 + media: Media.images(images: uploadedImageMaps), 843 + createdAt: DateTime.now().toUtc(), 844 + ); 879 845 880 - _logger.i('Image post created successfully: ${result.data.uri}'); 846 + final result = await _client.repo.createRecord( 847 + collection: 'so.sprk.feed.post', 848 + record: record.toJson(), 849 + ); 881 850 882 - // Crosspost to Bluesky if enabled 883 - if (crosspostToBsky) { 884 - try { 885 - await _crosspostToBlueSky(text, uploadedImageMaps, result.data as RepoStrongRef, altTexts); 886 - } catch (e) { 887 - _logger.w('Failed to crosspost to Bluesky: $e'); 888 - // Don't fail the entire operation if Bluesky crossposting fails 889 - } 890 - } 851 + _logger.i('Image post created successfully: ${result.uri}'); 891 852 892 - return result.data as RepoStrongRef; 893 - } else { 894 - _logger.e('AtProto not initialized'); 895 - throw Exception('AtProto not initialized'); 896 - } 897 - }); 853 + // Crosspost to Bluesky if enabled 854 + if (crosspostToBsky) { 855 + try { 856 + await _crosspostToBlueSky(text, uploadedImageMaps, result, altTexts); 857 + } catch (e) { 858 + _logger.w('Failed to crosspost to Bluesky: $e'); 859 + // Don't fail the entire operation if Bluesky crossposting fails 860 + } 898 861 } 862 + 863 + return result; 899 864 } 900 865 901 866 /// Helper to upload multiple images, stripping EXIF, and return a list of JSON maps for embedding ··· 1114 1079 facets: facets, 1115 1080 ); 1116 1081 1117 - final bskyAtProto = _client.authRepository.atproto!; 1118 - final bskyResult = await bskyAtProto.repo.createRecord( 1119 - repo: _client.sprkDid, 1082 + final bskyResult = await _client.repo.createRecord( 1120 1083 collection: 'app.bsky.feed.post', 1121 1084 record: bskyPost.toJson(), 1122 1085 rkey: sparkPostData.uri.rkey, 1123 1086 ); 1124 1087 1125 - _logger.i('Successfully crossposted to Bluesky: ${bskyResult.data.uri}'); 1088 + _logger.i('Successfully crossposted to Bluesky: ${bskyResult.uri}'); 1126 1089 } 1127 1090 1128 1091 /// Prepare text for Bluesky post, handling link addition and truncation ··· 1156 1119 Future<bool> deletePost(AtUri postUri) async { 1157 1120 _logger.d('Deleting post with URI: $postUri'); 1158 1121 1159 - return _client.executeWithRetry(() async { 1160 - if (!_client.authRepository.isAuthenticated) { 1161 - _logger.w('Not authenticated'); 1162 - throw Exception('Not authenticated'); 1163 - } 1164 - 1165 - final atproto = _client.authRepository.atproto; 1166 - if (atproto == null) { 1167 - _logger.e('AtProto not initialized'); 1168 - throw Exception('AtProto not initialized'); 1169 - } 1170 - 1171 - try { 1172 - final response = await atproto.repo.deleteRecord( 1173 - repo: _client.sprkDid, 1174 - collection: postUri.collection.toString(), 1175 - rkey: postUri.rkey, 1176 - ); 1177 - 1178 - switch (response.status.code) { 1179 - case 200: 1180 - _logger.i('Post deleted successfully: $postUri'); 1181 - return true; 1182 - default: 1183 - _logger.e('Failed to delete post: ${response.status.code}'); 1184 - return false; 1185 - } 1186 - } catch (e) { 1187 - _logger.e('Error deleting post', error: e); 1188 - return false; 1189 - } 1190 - }); 1122 + try { 1123 + await _client.repo.deleteRecord(uri: postUri); 1124 + _logger.i('Post deleted successfully: $postUri'); 1125 + return true; 1126 + } catch (e) { 1127 + _logger.e('Error deleting post', error: e); 1128 + return false; 1129 + } 1191 1130 } 1192 1131 1193 1132 @override ··· 1201 1140 }) async { 1202 1141 _logger.d('Posting video with description: $text'); 1203 1142 1204 - return _client.executeWithRetry(() async { 1205 - if (!_client.authRepository.isAuthenticated) { 1206 - _logger.w('Not authenticated'); 1207 - throw Exception('Not authenticated'); 1208 - } 1209 - 1210 - final record = PostRecord( 1211 - caption: CaptionRef(text: text, facets: []), 1212 - media: Media.video(video: blob, alt: alt), 1213 - createdAt: DateTime.now().toUtc(), 1214 - langs: langs, 1215 - selfLabels: selfLabels, 1216 - tags: tags, 1217 - ); 1143 + final record = PostRecord( 1144 + caption: CaptionRef(text: text, facets: []), 1145 + media: Media.video(video: blob, alt: alt), 1146 + createdAt: DateTime.now().toUtc(), 1147 + langs: langs, 1148 + selfLabels: selfLabels, 1149 + tags: tags, 1150 + ); 1218 1151 1219 - // Create the post record 1220 - final response = await _client.authRepository.atproto!.repo.createRecord( 1221 - repo: _client.sprkDid, 1222 - collection: 'so.sprk.feed.post', 1223 - record: record.toJson(), 1224 - ); 1152 + final result = await _client.repo.createRecord( 1153 + collection: 'so.sprk.feed.post', 1154 + record: record.toJson(), 1155 + ); 1225 1156 1226 - if (response.status == HttpStatus.ok) { 1227 - _logger.i('Video posted successfully: ${response.data.uri}'); 1228 - return response.data as RepoStrongRef; 1229 - } else { 1230 - _logger.e('Failed to post video: ${response.status} ${response.data}'); 1231 - throw Exception('Failed to post video: ${response.status} ${response.data}'); 1232 - } 1233 - }); 1157 + _logger.i('Video posted successfully: ${result.uri}'); 1158 + return result; 1234 1159 } 1235 1160 1236 1161 @override
+5 -22
lib/src/core/network/atproto/data/repositories/graph_repository_impl.dart
··· 122 122 123 123 final followRecord = {r'$type': collection, 'subject': did, 'createdAt': DateTime.now().toUtc().toIso8601String()}; 124 124 125 - final result = await atproto.repo.createRecord(repo: sessionDid, collection: collection, record: followRecord); 125 + final result = await _client.repo.createRecord(collection: collection, record: followRecord, repo: sessionDid); 126 126 127 - _logger.i('User followed successfully with $collection: ${result.data.uri}'); 127 + _logger.i('User followed successfully with $collection: ${result.uri}'); 128 128 129 - return FollowUserResponse(uri: result.data.uri.toString(), cid: result.data.cid); 129 + return FollowUserResponse(uri: result.uri.toString(), cid: result.cid); 130 130 } catch (e) { 131 131 _logger.e('Error in followUser', error: e); 132 132 rethrow; ··· 137 137 @override 138 138 Future<void> unfollowUser(AtUri followUri) async { 139 139 _logger.d('Unfollowing user with follow URI: $followUri'); 140 - return _client.executeWithRetry(() async { 141 - if (!_client.authRepository.isAuthenticated) { 142 - _logger.w('Not authenticated'); 143 - throw Exception('Not authenticated'); 144 - } 145 - 146 - final atproto = _client.authRepository.atproto; 147 - if (atproto == null) { 148 - _logger.e('AtProto not initialized'); 149 - throw Exception('AtProto not initialized'); 150 - } 151 - 152 - await atproto.repo.deleteRecord( 153 - repo: followUri.hostname, 154 - collection: followUri.collection.toString(), 155 - rkey: followUri.rkey, 156 - ); 157 - _logger.i('User unfollowed successfully'); 158 - }); 140 + await _client.repo.deleteRecord(uri: followUri, skipBskyCrosspostCleanup: true); 141 + _logger.i('User unfollowed successfully'); 159 142 } 160 143 161 144 @override
+10 -2
lib/src/core/network/atproto/data/repositories/repo_repository.dart
··· 20 20 /// 21 21 /// [collection] The NSID of the collection to create the record in 22 22 /// [record] The record data to create 23 - Future<RepoStrongRef> createRecord({required String collection, required Map<String, dynamic> record, String? rkey}); 23 + /// [rkey] Optional record key 24 + /// [repo] Optional DID of the repo (defaults to current user's DID if not provided) 25 + Future<RepoStrongRef> createRecord({ 26 + required String collection, 27 + required Map<String, dynamic> record, 28 + String? rkey, 29 + String? repo, 30 + }); 24 31 25 32 /// Delete a record from the repository 26 33 /// 27 34 /// [uri] The URI of the record to delete 28 - Future<void> deleteRecord({required AtUri uri}); 35 + /// [skipBskyCrosspostCleanup] If true, skips attempting to delete Bluesky crosspost 36 + Future<void> deleteRecord({required AtUri uri, bool skipBskyCrosspostCleanup = false}); 29 37 30 38 /// Upload a blob to the repository 31 39 ///
+35 -31
lib/src/core/network/atproto/data/repositories/repo_repository_impl.dart
··· 67 67 } 68 68 69 69 @override 70 - Future<RepoStrongRef> createRecord({required String collection, required Map<String, dynamic> record, String? rkey}) async { 70 + Future<RepoStrongRef> createRecord({ 71 + required String collection, 72 + required Map<String, dynamic> record, 73 + String? rkey, 74 + String? repo, 75 + }) async { 71 76 _logger.d('Creating record in collection: $collection'); 72 77 return _client.executeWithRetry(() async { 73 - if (!_client.authRepository.isAuthenticated) { 74 - _logger.w('Not authenticated'); 75 - throw Exception('Not authenticated'); 76 - } 77 - 78 78 final atproto = _client.authRepository.atproto; 79 79 if (atproto == null) { 80 80 _logger.e('AtProto not initialized'); 81 81 throw Exception('AtProto not initialized'); 82 82 } 83 83 84 - final result = await atproto.repo.createRecord(repo: _client.sprkDid, collection: collection, record: record, rkey: rkey); 84 + // Use provided repo DID or fall back to session DID 85 + final repoDid = repo ?? _client.authRepository.session?.did; 86 + if (repoDid == null) { 87 + _logger.e('User session DID not available'); 88 + throw Exception('User session DID not available'); 89 + } 90 + 91 + final result = await atproto.repo.createRecord(repo: repoDid, collection: collection, record: record, rkey: rkey); 85 92 _logger.d('Record created successfully'); 86 93 return RepoStrongRef(uri: result.data.uri, cid: result.data.cid); 87 94 }); 88 95 } 89 96 90 97 @override 91 - Future<void> deleteRecord({required AtUri uri}) async { 98 + Future<void> deleteRecord({required AtUri uri, bool skipBskyCrosspostCleanup = false}) async { 92 99 _logger.d('Deleting record at URI: $uri'); 93 100 return _client.executeWithRetry(() async { 94 - if (!_client.authRepository.isAuthenticated) { 95 - _logger.w('Not authenticated'); 96 - throw Exception('Not authenticated'); 97 - } 98 - 99 101 final atproto = _client.authRepository.atproto; 100 102 if (atproto == null) { 101 103 _logger.e('AtProto not initialized'); ··· 105 107 await atproto.repo.deleteRecord(repo: uri.hostname, collection: uri.collection.toString(), rkey: uri.rkey); 106 108 _logger.d('Record deleted successfully'); 107 109 108 - // Delete cross-posted Bluesky counterpart if it exists 109 - try { 110 - final did = uri.hostname; 111 - final rkey = uri.rkey; 112 - final blueskyUri = AtUri.parse('at://$did/app.bsky.feed.post/$rkey'); 110 + // Delete cross-posted Bluesky counterpart if it exists (only for posts) 111 + if (!skipBskyCrosspostCleanup) { 112 + try { 113 + final did = uri.hostname; 114 + final rkey = uri.rkey; 115 + final blueskyUri = AtUri.parse('at://$did/app.bsky.feed.post/$rkey'); 113 116 114 - _logger.d('Attempting to delete Bluesky counterpart post: $blueskyUri'); 117 + _logger.d('Attempting to delete Bluesky counterpart post: $blueskyUri'); 115 118 116 - try { 117 - await atproto.repo.deleteRecord( 118 - repo: blueskyUri.hostname, 119 - collection: blueskyUri.collection.toString(), 120 - rkey: blueskyUri.rkey, 121 - ); 122 - _logger.d('Bluesky counterpart post deleted successfully'); 119 + try { 120 + await atproto.repo.deleteRecord( 121 + repo: blueskyUri.hostname, 122 + collection: blueskyUri.collection.toString(), 123 + rkey: blueskyUri.rkey, 124 + ); 125 + _logger.d('Bluesky counterpart post deleted successfully'); 126 + } catch (e) { 127 + // Ignore errors like 404 – it simply means the counterpart does not exist. 128 + _logger.w('Bluesky counterpart post not found or deletion failed', error: e); 129 + } 123 130 } catch (e) { 124 - // Ignore errors like 404 – it simply means the counterpart does not exist. 125 - _logger.w('Bluesky counterpart post not found or deletion failed', error: e); 131 + // Best-effort only – do not fail original deletion. 132 + _logger.w('Failed during Bluesky cross-post deletion cleanup', error: e); 126 133 } 127 - } catch (e) { 128 - // Best-effort only – do not fail original deletion. 129 - _logger.w('Failed during Bluesky cross-post deletion cleanup', error: e); 130 134 } 131 135 }); 132 136 }
+12 -25
lib/src/core/network/atproto/data/repositories/sound_repository_impl.dart
··· 23 23 AudioDetails? details, 24 24 }) async { 25 25 _logger.d('Creating sound record with title: $title'); 26 - return _client.executeWithRetry(() async { 27 - if (!_client.authRepository.isAuthenticated) { 28 - _logger.w('Not authenticated'); 29 - throw Exception('Not authenticated'); 30 - } 31 26 32 - final atproto = _client.authRepository.atproto; 33 - if (atproto == null) { 34 - _logger.e('AtProto not initialized'); 35 - throw Exception('AtProto not initialized'); 36 - } 27 + final audioRecord = AudioRecord( 28 + sound: sound, 29 + title: title, 30 + createdAt: DateTime.now().toUtc(), 31 + details: details, 32 + ); 37 33 38 - final audioRecord = AudioRecord( 39 - sound: sound, 40 - title: title, 41 - createdAt: DateTime.now().toUtc(), 42 - details: details, 43 - ); 44 - 45 - final result = await atproto.repo.createRecord( 46 - repo: _client.sprkDid, 47 - collection: 'so.sprk.sound.audio', 48 - record: audioRecord.toJson(), 49 - ); 34 + final result = await _client.repo.createRecord( 35 + collection: 'so.sprk.sound.audio', 36 + record: audioRecord.toJson(), 37 + ); 50 38 51 - _logger.i('Sound record created successfully: ${result.data.uri}'); 52 - return result.data as RepoStrongRef; 53 - }); 39 + _logger.i('Sound record created successfully: ${result.uri}'); 40 + return result; 54 41 } 55 42 56 43 @override
+6 -19
lib/src/core/network/atproto/data/repositories/story_repository_impl.dart
··· 158 158 } 159 159 160 160 @override 161 - Future<RepoStrongRef> postStory(Media media, {List<SelfLabel>? selfLabels, List<String>? tags}) { 162 - return _client.executeWithRetry(() async { 163 - if (!_client.authRepository.isAuthenticated) { 164 - throw Exception('Not authenticated'); 165 - } 161 + Future<RepoStrongRef> postStory(Media media, {List<SelfLabel>? selfLabels, List<String>? tags}) async { 162 + final record = StoryRecord(createdAt: DateTime.now().toUtc(), media: media, tags: tags, labels: selfLabels); 166 163 167 - final record = StoryRecord(createdAt: DateTime.now().toUtc(), media: media, tags: tags, labels: selfLabels); 168 - 169 - final response = await _client.authRepository.atproto!.repo.createRecord( 170 - repo: _client.sprkDid, 171 - collection: 'so.sprk.story.post', 172 - record: record.toJson(), 173 - ); 174 - 175 - if (response.status.code != 200) { 176 - throw Exception('Failed to post story: ${response.status} ${response.data}'); 177 - } 178 - 179 - return response.data as RepoStrongRef; 180 - }); 164 + return _client.repo.createRecord( 165 + collection: 'so.sprk.story.post', 166 + record: record.toJson(), 167 + ); 181 168 } 182 169 }
+8 -24
lib/src/features/posting/providers/video_upload_provider.dart
··· 2 2 import 'package:bluesky/com_atproto_repo_strongref.dart'; 3 3 import 'package:get_it/get_it.dart'; 4 4 import 'package:riverpod_annotation/riverpod_annotation.dart'; 5 - import 'package:sparksocial/src/core/auth/data/repositories/auth_repository.dart'; 6 5 import 'package:sparksocial/src/core/network/atproto/atproto.dart'; 7 6 import 'package:sparksocial/src/core/utils/logging/log_service.dart'; 8 7 import 'package:sparksocial/src/features/posting/providers/post_story.dart'; ··· 41 40 final logger = GetIt.I<LogService>().getLogger('Posting Video'); 42 41 try { 43 42 logger.d('Posting video (size=${blob.size}, crosspost=$crosspostToBsky, sound=${soundRef?.uri})'); 44 - final authRepository = GetIt.I<AuthRepository>(); 45 - final authAtProto = authRepository.atproto; 46 - if (authAtProto == null || authAtProto.session == null) { 47 - throw Exception('AtProto not initialized'); 48 - } 49 43 50 44 final postRecord = PostRecord( 51 45 caption: CaptionRef(text: description.isNotEmpty ? description : '', facets: []), ··· 54 48 sound: soundRef, 55 49 ); 56 50 57 - final recordRes = await authAtProto.repo.createRecord( 58 - repo: authAtProto.session!.did, 51 + final result = await GetIt.I<SprkRepository>().repo.createRecord( 59 52 collection: 'so.sprk.feed.post', 60 53 record: postRecord.toJson(), 61 54 ); 62 55 63 - if (recordRes.status != HttpStatus.ok) { 64 - throw Exception('Failed to post video: ${recordRes.status}'); 65 - } 66 - 67 56 if (crosspostToBsky) { 68 57 try { 69 - await _crosspostVideoToBlueSky(ref, description, blob, altText, recordRes.data.uri.rkey); 58 + await _crosspostVideoToBlueSky(ref, description, blob, altText, result.uri.rkey); 70 59 } catch (e, s) { 71 60 logger.w('Crosspost to Bluesky failed: $e', error: e, stackTrace: s); 72 61 } 73 62 } 74 - logger.i('Video posted successfully: ${recordRes.data.uri}'); 75 - return recordRes.data as RepoStrongRef; 63 + logger.i('Video posted successfully: ${result.uri}'); 64 + return result; 76 65 } catch (error, stackTrace) { 77 66 logger.e('Error posting video', error: error, stackTrace: stackTrace); 78 67 } ··· 157 146 String rkey, 158 147 ) async { 159 148 final logger = GetIt.I<LogService>().getLogger('Crosspost Video'); 160 - final authRepository = GetIt.I<AuthRepository>(); 161 149 logger.d('Crossposting video to Bluesky'); 162 - final session = authRepository.session; 163 - if (session == null) { 164 - throw Exception('No session available for Bluesky crosspost'); 165 - } 150 + 166 151 final bskyPostRecord = <String, dynamic>{ 167 152 r'$type': 'app.bsky.feed.post', 168 153 'text': text, 169 154 'embed': {r'$type': 'app.bsky.embed.video', 'video': blob.toJson(), 'alt': altText}, 170 155 'createdAt': DateTime.now().toUtc().toIso8601String(), 171 156 }; 157 + 172 158 try { 173 - final bskyAtProto = authRepository.atproto!; 174 - final bskyResult = await bskyAtProto.repo.createRecord( 175 - repo: session.did, 159 + final result = await GetIt.I<SprkRepository>().repo.createRecord( 176 160 collection: 'app.bsky.feed.post', 177 161 record: bskyPostRecord, 178 162 rkey: rkey, 179 163 ); 180 - logger.i('Crossposted video to Bluesky: ${bskyResult.data.uri}'); 164 + logger.i('Crossposted video to Bluesky: ${result.uri}'); 181 165 } catch (e, s) { 182 166 logger.w('Failed to crosspost video: $e', error: e, stackTrace: s); 183 167 }