A cheap attempt at a native Bluesky client for Android
8
fork

Configure Feed

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

Bluesky: Split appview and PDS clients to fix Blacksky proxy errors

The atproto-proxy header was set on all requests, but PDS procedures
(createRecord, deleteRecord, putRecord, uploadBlob, muteActor, etc.)
should not be proxied. When Blacksky was selected as appview, the PDS
tried to proxy writes to Blacksky which returned XRPCNotSupported.

Add a separate pdsClient without the proxy header for all PDS-native
calls. The appview client (with proxy header) is used only for queries
like getTimeline, getProfile, searchPosts, etc.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

geesawra 1b07739d d105927a

+54 -20
+54 -20
app/src/main/java/industries/geesawra/monarch/datalayer/Bluesky.kt
··· 244 244 val service: List<Service> 245 245 ) 246 246 247 - var client: AuthenticatedXrpcBlueskyApi? = null 247 + var client: AuthenticatedXrpcBlueskyApi? = null // appview queries (has atproto-proxy) 248 + var pdsClient: AuthenticatedXrpcBlueskyApi? = null // PDS procedures (no atproto-proxy) 248 249 var session: SessionData? = null 249 250 var createMutex: Mutex = Mutex() 250 251 var pdsURL: String? = null ··· 378 379 ) 379 380 session = null 380 381 this.client = null 382 + this.pdsClient = null 381 383 382 384 createMutex.unlock() 383 385 return Result.success(Unit) ··· 424 426 ) 425 427 } 426 428 429 + private fun mkPdsClient( 430 + pds: String, 431 + sessionData: SessionData, 432 + ): AuthenticatedXrpcBlueskyApi { 433 + val hc = HttpClient(OkHttp) { 434 + defaultRequest { 435 + url(pds) 436 + } 437 + install(HttpTimeout) { 438 + requestTimeoutMillis = 15000 439 + connectTimeoutMillis = 15000 440 + socketTimeoutMillis = 15000 441 + } 442 + install(HttpRequestRetry) { 443 + maxRetries = 5 444 + retryIf { _, response -> 445 + response.status.value == 503 446 + } 447 + retryOnExceptionIf { _, cause -> 448 + cause.message?.contains("upstream service unavailable", ignoreCase = true) == true 449 + } 450 + exponentialDelay() 451 + } 452 + } 453 + 454 + return AuthenticatedXrpcBlueskyApi( 455 + hc, 456 + BlueskyAuthPlugin.Tokens(sessionData.accessJwt, sessionData.refreshJwt) 457 + ) 458 + } 459 + 427 460 private suspend fun refreshIfNeeded( 428 461 pdsURL: String, 429 462 appviewProxy: String, ··· 512 545 suspend fun create(): Result<Unit> { 513 546 return runCatching { 514 547 createMutex.lock() 515 - if (session != null && client != null && pdsURL != null) { 548 + if (session != null && client != null && pdsClient != null && pdsURL != null) { 516 549 createMutex.unlock() 517 550 return Result.success(Unit) 518 551 } ··· 545 578 } 546 579 this.pdsURL = pdsURL 547 580 581 + this.pdsClient = mkPdsClient(pdsURL, sessionData) 548 582 this.client = mkClient( 549 583 pdsURL, 550 584 appviewProxy, ··· 697 731 ) 698 732 ) 699 733 700 - val postRes = client!!.createRecord( 734 + val postRes = pdsClient!!.createRecord( 701 735 CreateRecordRequest( 702 736 repo = session!!.handle, // Use handle from the session 703 737 collection = Nsid("app.bsky.feed.post"), ··· 717 751 return Result.failure(LoginException(it.message)) 718 752 } 719 753 720 - val ret = client!!.getRecord( 754 + val ret = pdsClient!!.getRecord( 721 755 GetRecordQueryParams( 722 756 repo = uri.did(), 723 757 collection = uri.collection(), ··· 774 808 if (!response.status.isSuccess()) return null 775 809 val bytes: ByteArray = response.body() 776 810 httpClient.close() 777 - val uploadResponse = client!!.uploadBlob(bytes) 811 + val uploadResponse = pdsClient!!.uploadBlob(bytes) 778 812 return when (uploadResponse) { 779 813 is AtpResponse.Failure<*> -> null 780 814 is AtpResponse.Success<UploadBlobResponse> -> uploadResponse.response.blob ··· 806 840 return@run c 807 841 } 808 842 809 - val blob = client!!.uploadBlob(compressedImage.data) 843 + val blob = pdsClient!!.uploadBlob(compressedImage.data) 810 844 when (blob) { 811 845 is AtpResponse.Failure<*> -> return Result.failure(Exception("Failed uploading image: ${blob.error}")) 812 846 is AtpResponse.Success<UploadBlobResponse> -> { ··· 855 889 856 890 val did = Did("did:web:" + pdsURL!!.toUri().host!!) 857 891 858 - val uploadVideoTicket = client!!.getServiceAuth( 892 + val uploadVideoTicket = pdsClient!!.getServiceAuth( 859 893 GetServiceAuthQueryParams( 860 894 aud = did, 861 895 exp = Clock.System.now().plus(Duration.parse("30m")).epochSeconds, ··· 976 1010 create().onFailure { 977 1011 return Result.failure(LoginException(it.message)) 978 1012 } 979 - val prefs = client!!.getPreferences().requireResponse() 1013 + val prefs = pdsClient!!.getPreferences().requireResponse() 980 1014 val feedUris = (prefs.preferences.first { 981 1015 when (it) { 982 1016 is PreferencesUnion.SavedFeedsPrefV2 -> true ··· 998 1032 999 1033 suspend fun subscribedLabelers(): Result<Map<Did?, GetServicesResponseViewUnion.LabelerViewDetailed?>> { 1000 1034 return runCatching { 1001 - val prefs = client!!.getPreferences().requireResponse() 1035 + val prefs = pdsClient!!.getPreferences().requireResponse() 1002 1036 val labelers = (prefs.preferences.first { 1003 1037 when (it) { 1004 1038 is PreferencesUnion.LabelersPref -> true ··· 1058 1092 return Result.failure(LoginException(it.message)) 1059 1093 } 1060 1094 1061 - val ret = client!!.updateSeen( 1095 + val ret = pdsClient!!.updateSeen( 1062 1096 UpdateSeenRequest( 1063 1097 seenAt = Clock.System.now().toDeprecatedInstant(), 1064 1098 ) ··· 1104 1138 ) 1105 1139 1106 1140 1107 - val likeRes = client!!.createRecord( 1141 + val likeRes = pdsClient!!.createRecord( 1108 1142 CreateRecordRequest( 1109 1143 repo = session!!.handle, 1110 1144 collection = Nsid("app.bsky.feed.like"), ··· 1135 1169 ) 1136 1170 1137 1171 1138 - val likeRes = client!!.createRecord( 1172 + val likeRes = pdsClient!!.createRecord( 1139 1173 CreateRecordRequest( 1140 1174 repo = session!!.handle, 1141 1175 collection = Nsid("app.bsky.feed.repost"), ··· 1240 1274 ) 1241 1275 ) 1242 1276 1243 - val followRes = client!!.createRecord( 1277 + val followRes = pdsClient!!.createRecord( 1244 1278 CreateRecordRequest( 1245 1279 repo = session!!.handle, 1246 1280 collection = Nsid("app.bsky.graph.follow"), ··· 1267 1301 return Result.failure(LoginException(it.message)) 1268 1302 } 1269 1303 1270 - val ret = client!!.muteActor( 1304 + val ret = pdsClient!!.muteActor( 1271 1305 app.bsky.graph.MuteActorRequest(actor = did) 1272 1306 ) 1273 1307 ··· 1284 1318 return Result.failure(LoginException(it.message)) 1285 1319 } 1286 1320 1287 - val ret = client!!.getRecord( 1321 + val ret = pdsClient!!.getRecord( 1288 1322 GetRecordQueryParams( 1289 1323 repo = session!!.did, 1290 1324 collection = Nsid("app.bsky.actor.profile"), ··· 1321 1355 if (avatarUri != null) { 1322 1356 val compressor = Compressor(context) 1323 1357 val compressed = compressor.compressImage(avatarUri, 950000) 1324 - val uploaded = client!!.uploadBlob(compressed.data) 1358 + val uploaded = pdsClient!!.uploadBlob(compressed.data) 1325 1359 avatarBlob = when (uploaded) { 1326 1360 is AtpResponse.Failure<*> -> return Result.failure(Exception("Failed uploading avatar: ${uploaded.error}")) 1327 1361 is AtpResponse.Success<UploadBlobResponse> -> uploaded.response.blob ··· 1331 1365 if (bannerUri != null) { 1332 1366 val compressor = Compressor(context) 1333 1367 val compressed = compressor.compressImage(bannerUri, 950000) 1334 - val uploaded = client!!.uploadBlob(compressed.data) 1368 + val uploaded = pdsClient!!.uploadBlob(compressed.data) 1335 1369 bannerBlob = when (uploaded) { 1336 1370 is AtpResponse.Failure<*> -> return Result.failure(Exception("Failed uploading banner: ${uploaded.error}")) 1337 1371 is AtpResponse.Success<UploadBlobResponse> -> uploaded.response.blob ··· 1350 1384 1351 1385 val record = BlueskyJson.encodeAsJsonContent(updatedProfile) 1352 1386 1353 - val ret = client!!.putRecord( 1387 + val ret = pdsClient!!.putRecord( 1354 1388 com.atproto.repo.PutRecordRequest( 1355 1389 repo = session!!.did, 1356 1390 collection = Nsid("app.bsky.actor.profile"), ··· 1428 1462 return Result.failure(LoginException(it.message)) 1429 1463 } 1430 1464 1431 - val ret = client!!.unmuteActor( 1465 + val ret = pdsClient!!.unmuteActor( 1432 1466 app.bsky.graph.UnmuteActorRequest(actor = did) 1433 1467 ) 1434 1468 ··· 1445 1479 return Result.failure(LoginException(it.message)) 1446 1480 } 1447 1481 1448 - val delRes = client!!.deleteRecord( 1482 + val delRes = pdsClient!!.deleteRecord( 1449 1483 DeleteRecordRequest( 1450 1484 repo = session!!.handle, 1451 1485 collection = Nsid(collection),