···3939 pdsUrl = buildUrl {
4040 protocol = URLProtocol.HTTPS
4141 host = ownerPds
4242- }
4242+ },
4343 )
44444545 // Make sure the feed generator record exists and points to the current
+46-15
app/src/main/kotlin/BskyApi.kt
···88import io.ktor.client.call.*
99import io.ktor.client.engine.cio.*
1010import io.ktor.client.plugins.*
1111+import io.ktor.client.plugins.auth.*
1212+import io.ktor.client.plugins.auth.providers.*
1113import io.ktor.client.plugins.contentnegotiation.*
1214import io.ktor.client.plugins.logging.*
1315import io.ktor.client.request.*
···1820import kotlinx.serialization.json.Json
19212022class BskyApi(
2121- private var pdsUrl: Url = Url("https://bsky.social"),
2323+ private val pdsUrl: Url = Url("https://bsky.social"),
2424+2525+ private val bearerTokens: MutableList<BearerTokens> = mutableListOf(),
22262327 private val httpClient: HttpClient = HttpClient(CIO) {
2428 install(ContentNegotiation) {
···30343135 install(Logging)
32363737+ install(Auth) {
3838+ bearer {
3939+ loadTokens {
4040+ bearerTokens.lastOrNull()
4141+ }
4242+4343+ refreshTokens {
4444+ val currentRefreshToken = bearerTokens.lastOrNull()?.refreshToken ?: return@refreshTokens null
4545+4646+ @Serializable
4747+ data class Response(val accessJwt: String, val refreshJwt: String)
4848+4949+ val refreshSessionResponse = client.post("com.atproto.server.refreshSession") {
5050+ header("Authorization", "Bearer $currentRefreshToken")
5151+ markAsRefreshTokenRequest()
5252+ }
5353+5454+ when (refreshSessionResponse.status) {
5555+ HttpStatusCode.OK -> {
5656+ val refreshSessionTokens = refreshSessionResponse.body<Response>()
5757+ val newBearerTokens =
5858+ BearerTokens(refreshSessionTokens.accessJwt, refreshSessionTokens.refreshJwt)
5959+6060+ bearerTokens.addLast(newBearerTokens)
6161+6262+ return@refreshTokens newBearerTokens
6363+ }
6464+6565+ HttpStatusCode.BadRequest,
6666+ HttpStatusCode.Unauthorized -> return@refreshTokens null
6767+6868+ else -> return@refreshTokens null
6969+ }
7070+ }
7171+ }
7272+ }
7373+3374 defaultRequest {
3475 url {
3576 protocol = pdsUrl.protocol
···3980 }
4081 },
4182) {
4242-4343-4444- data class AuthTokens(
4545- val accessJwt: String,
4646- val refreshJwt: String,
4747- val did: String,
4848- )
4949-5050- private var authTokens: AuthTokens? = null
5151-5283 @Serializable
5384 data class ErrorResponse(
5485 val error: String,
···70101 when (response.status) {
71102 HttpStatusCode.OK -> {
72103 val tokens: Response = response.body()
7373- this.authTokens = AuthTokens(tokens.accessJwt, tokens.refreshJwt, tokens.did)
104104+ bearerTokens.addLast(BearerTokens(tokens.accessJwt, tokens.refreshJwt))
74105 }
7510676107 HttpStatusCode.BadRequest,
···111142 @Serializable
112143 data class Request(
113144 val repo: String,
145145+ val collection: String,
114146 val rkey: String,
115147 val record: Generator,
116116- val collection: String = "app.bsky.feed.generator",
117148 )
118149119150 val response = httpClient.post("com.atproto.repo.putRecord") {
120151 contentType(ContentType.Application.Json)
121121- setBody(Request(repo, rkey, record))
152152+ setBody(Request(repo, "app.bsky.feed.generator", rkey, record))
122153 }
123154124155 when (response.status) {
···175206 else -> throw RuntimeException("Unexpected response received: ${response.bodyAsText()}")
176207 }
177208 }
178178-}209209+}
+18-8
app/src/main/kotlin/DarkFeedApi.kt
···11package gay.averyrivers
2233import gay.averyrivers.lexicon.app.bsky.feed.FeedSkeleton
44+import gay.averyrivers.lexicon.app.bsky.feed.defs.PostView
45import gay.averyrivers.lexicon.app.bsky.feed.defs.SkeletonFeedPost
56import io.ktor.serialization.kotlinx.json.*
67import io.ktor.server.application.*
···6869 )
6970 }
70717171- // SOOOOO
7272- // I NEED TO CHECK THE SELF LABEL (at://did:plc:vwivwqztbf6pmkgss3nv2scy/app.bsky.feed.post/3la5sxh4ica2r)
7373- // AND THE MODERATION.BSKY.APP LABEL (at://did:plc:ujrpupcjf22a4riwjbupdv42/app.bsky.feed.post/3la5qntezsu2v)
7474-7572 private suspend fun handleGetFeedSkeleton(call: RoutingCall) {
7673 // TODO: Get requestor's DID from Authorization header.
7774 call.respond(buildFeedSkeleton("did:plc:zhxv5pxpmojhnvaqy4mwailv"))
7875 }
79768077 private suspend fun buildFeedSkeleton(requestor: String): FeedSkeleton {
7878+ val actorLikes = bskyApi.getLikesByActor(requestor)
7979+ .first
8080+ .map { likeRef -> likeRef.value.subject.uri }
8181+8282+ val labeledPosts = actorLikes
8383+ .chunked(25)
8484+ .map { chunkedActorLikes ->
8585+ bskyApi.getPostLabels(chunkedActorLikes)
8686+ .filter { post ->
8787+ post.labels?.any { label -> listOf("porn", "sexual").contains(label.value) } ?: false
8888+ }
8989+ }
9090+ .flatten()
9191+9292+8193 return FeedSkeleton(
8282- feed = bskyApi.getLikesByActor(requestor)
8383- .first
8484- .map { likeRef -> SkeletonFeedPost(post = likeRef.value.subject.uri) }
9494+ feed = labeledPosts.map { post -> SkeletonFeedPost(post = post.uri) }
8595 )
8696 }
8787-}9797+}