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

Configure Feed

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

Bluesky: fix video upload for good

We should stop relying on that bsky lib smh.

geesawra 6e282191 b36f9c72

+55 -16
+55 -16
app/src/main/java/industries/geesawra/monarch/datalayer/Bluesky.kt
··· 24 24 import app.bsky.feed.Repost 25 25 import app.bsky.video.GetJobStatusQueryParams 26 26 import app.bsky.video.GetJobStatusResponse 27 + import app.bsky.video.JobStatus 27 28 import app.bsky.video.State 28 29 import app.bsky.video.UploadVideoResponse 29 30 import com.atproto.identity.ResolveHandleQueryParams ··· 43 44 import io.ktor.client.call.body 44 45 import io.ktor.client.engine.okhttp.OkHttp 45 46 import io.ktor.client.plugins.HttpTimeout 47 + import io.ktor.client.plugins.contentnegotiation.ContentNegotiation 46 48 import io.ktor.client.plugins.defaultRequest 47 49 import io.ktor.client.request.get 48 50 import io.ktor.client.request.post 51 + import io.ktor.client.request.setBody 52 + import io.ktor.http.ContentType 49 53 import io.ktor.http.HttpStatusCode 50 54 import io.ktor.http.URLProtocol 51 55 import io.ktor.http.path 56 + import io.ktor.serialization.kotlinx.KotlinxSerializationConverter 52 57 import kotlinx.coroutines.delay 53 58 import kotlinx.coroutines.flow.Flow 54 59 import kotlinx.coroutines.flow.first ··· 56 61 import kotlinx.coroutines.sync.Mutex 57 62 import kotlinx.datetime.Clock 58 63 import kotlinx.serialization.Serializable 64 + import kotlinx.serialization.json.Json 59 65 import sh.christian.ozone.BlueskyJson 60 66 import sh.christian.ozone.XrpcBlueskyApi 61 67 import sh.christian.ozone.api.AtUri ··· 532 538 533 539 val uploadedBlobs = mutableListOf<Blob>() 534 540 541 + val did = Did("did:web:" + pdsURL!!.toUri().host!!) 542 + 535 543 val uploadVideoTicket = client!!.getServiceAuth( 536 544 GetServiceAuthQueryParams( 537 - aud = Did("did:web:" + pdsURL!!.toUri().host!!), 545 + aud = did, 538 546 exp = Clock.System.now().plus(Duration.parse("30m")).epochSeconds, 539 547 lxm = Nsid("com.atproto.repo.uploadBlob"), 540 548 ) 541 549 ) 542 550 543 551 val serviceAuth = when (uploadVideoTicket) { 544 - is AtpResponse.Failure<*> -> return Result.failure(Exception("Failed uploading video: ${uploadVideoTicket.error}")) 552 + is AtpResponse.Failure<*> -> return Result.failure(Exception("Failed requesting upload ticket: ${uploadVideoTicket.error}")) 545 553 is AtpResponse.Success<GetServiceAuthResponse> -> uploadVideoTicket.response.token 546 554 } 547 555 ··· 553 561 requestTimeoutMillis = 15000 554 562 connectTimeoutMillis = 15000 555 563 socketTimeoutMillis = 15000 564 + } 565 + install(ContentNegotiation) { 566 + register( 567 + ContentType.Any, KotlinxSerializationConverter( 568 + Json { 569 + prettyPrint = true 570 + isLenient = true 571 + ignoreUnknownKeys = true 572 + } 573 + ) 574 + ) 556 575 } 557 576 } 558 577 559 - val client = AuthenticatedXrpcBlueskyApi( 578 + val videoBskyAppClient = AuthenticatedXrpcBlueskyApi( 560 579 httpClient, 561 580 BlueskyAuthPlugin.Tokens(serviceAuth, serviceAuth) 562 581 ) ··· 564 583 val uploadRes = context.contentResolver.openInputStream(video)?.use { inputStream -> 565 584 val byteArray = inputStream.readBytes() 566 585 567 - val videoUploadTicket = client.uploadVideo(byteArray) 568 - when (videoUploadTicket) { 569 - is AtpResponse.Failure<*> -> return Result.failure(Exception("Failed uploading video: ${videoUploadTicket.error}")) 570 - is AtpResponse.Success<UploadVideoResponse> -> { 571 - return@use videoUploadTicket.response.jobStatus 586 + val rs = httpClient.post { 587 + headers["Authorization"] = "Bearer $serviceAuth" 588 + headers["Content-Type"] = "video/mp4" 589 + headers["Content-Length"] = byteArray.size.toString() 590 + url { 591 + protocol = URLProtocol.HTTPS 592 + path("/xrpc/app.bsky.video.uploadVideo") 593 + parameters.append("did", session!!.did.did) 594 + parameters.append( 595 + "name", 596 + "video_upload_${Clock.System.now().toEpochMilliseconds()}" 597 + ) 598 + } 599 + setBody(byteArray) 600 + } 601 + 602 + 603 + when (rs.status) { 604 + HttpStatusCode.OK -> { 605 + return@use rs.body<UploadVideoResponse>().jobStatus 606 + } 607 + 608 + HttpStatusCode.Conflict -> { 609 + // already uploaded once 610 + return@use rs.body<JobStatus>() 611 + } 612 + 613 + else -> { 614 + return Result.failure(Exception("Failed uploading video: status code ${rs.status}")) 572 615 } 573 616 } 574 617 } ··· 576 619 while (true) { 577 620 try { 578 621 val response = 579 - client.getJobStatus(GetJobStatusQueryParams(uploadRes!!.jobId)) 622 + videoBskyAppClient.getJobStatus(GetJobStatusQueryParams(uploadRes!!.jobId)) 580 623 581 624 val resp = when (response) { 582 625 is AtpResponse.Failure<*> -> return Result.failure( 583 - Exception("Failed uploading video: ${response.error}") 626 + Exception("Failed video processing job status check: ${response.error}") 584 627 ) 585 628 586 629 is AtpResponse.Success<GetJobStatusResponse> -> response.response.jobStatus ··· 592 635 } 593 636 594 637 when (resp.state) { 595 - State.JOBSTATECOMPLETED -> { 596 - uploadedBlobs.add(resp.blob!!) 597 - break 598 - } 599 - 600 - State.JOBSTATEFAILED -> return Result.failure(Exception("Failed uploading video, ${resp.error}: ${resp.message}")) 638 + State.JOBSTATECOMPLETED -> {} // ignore, as we check blobk anyway 639 + State.JOBSTATEFAILED -> return Result.failure(Exception("Video processing failed, ${resp.error}: ${resp.message}")) 601 640 is State.Unknown -> delay(1000) 602 641 } 603 642 } catch (e: Exception) {