Bluesky feed server - NSFW Likes

feat: Cache identity responses

Changed files
+54 -3
darkfeed
gradle
+1
darkfeed/build.gradle.kts
··· 21 implementation(libs.ktor.serialization.kotlinx.json) 22 implementation(libs.java.jwt) 23 implementation(libs.clikt) 24 } 25 26 java {
··· 21 implementation(libs.ktor.serialization.kotlinx.json) 22 implementation(libs.java.jwt) 23 implementation(libs.clikt) 24 + implementation(libs.kache) 25 } 26 27 java {
+3 -1
darkfeed/src/main/kotlin/Main.kt
··· 93 } catch (error: Exception) { 94 log.warn("Unknown failure occurred: {}", error.message) 95 null 96 - } finally { 97 log.warn("Authorized requests (e.g. updating feed generator record) will not be available") 98 } 99
··· 93 } catch (error: Exception) { 94 log.warn("Unknown failure occurred: {}", error.message) 95 null 96 + } 97 + 98 + if (authAccount == null) { 99 log.warn("Authorized requests (e.g. updating feed generator record) will not be available") 100 } 101
+46 -2
darkfeed/src/main/kotlin/api/AtpIdentityClient.kt
··· 1 package rs.averyrive.darkfeed.api 2 3 import io.ktor.client.* 4 import io.ktor.client.call.* 5 import io.ktor.client.engine.cio.* ··· 14 import kotlinx.serialization.json.Json 15 import org.slf4j.LoggerFactory 16 import rs.averyrive.darkfeed.api.model.* 17 18 /** Client that handles identity related requests for ATProto accounts. */ 19 class AtpIdentityClient { ··· 32 install(Logging) 33 } 34 35 /** 36 * Resolve a handle's DID. 37 * ··· 46 */ 47 suspend fun resolveDidFromHandle(handle: String): Did { 48 log.debug("Resolving DID for '{}'...", handle) 49 50 @Serializable 51 data class ResolveHandleResponse(val did: String) ··· 101 else -> throw RuntimeException("Received unexpected response: ${resolveHandleResponse.bodyAsText()}") 102 } 103 104 - log.debug("Resolved DID for '{}': {}", handle, did.toString()) 105 106 return did 107 } ··· 120 */ 121 suspend fun resolvePdsFromDid(did: Did): String { 122 log.debug("Resolving PDS for '{}'...", did.toString()) 123 124 // Get the DID document, using either `plc.directory` or 125 // `/.well-known/did.json` depending on the type of DID. ··· 166 ?.serviceEndpoint 167 ?: throw PdsNotFoundException(did) 168 169 - log.debug("Resolved PDS for '{}': {}", did.toString(), pdsHost) 170 171 return pdsHost 172 } 173 } 174
··· 1 package rs.averyrive.darkfeed.api 2 3 + import com.mayakapps.kache.InMemoryKache 4 import io.ktor.client.* 5 import io.ktor.client.call.* 6 import io.ktor.client.engine.cio.* ··· 15 import kotlinx.serialization.json.Json 16 import org.slf4j.LoggerFactory 17 import rs.averyrive.darkfeed.api.model.* 18 + import kotlin.time.Duration.Companion.hours 19 + 20 + // Specify size of each cache 21 + const val CACHE_SIZE: Long = 1 * 1024 * 1024 // 1 MB 22 23 /** Client that handles identity related requests for ATProto accounts. */ 24 class AtpIdentityClient { ··· 37 install(Logging) 38 } 39 40 + /** Cache of handle to DID relationships. */ 41 + private val handleDidCache = InMemoryKache<String, Did>(CACHE_SIZE) { 42 + expireAfterWriteDuration = 1.hours 43 + } 44 + 45 + /** Cache of DID to PDS host relationships. */ 46 + private val didPdsCache = InMemoryKache<Did, String>(CACHE_SIZE) { 47 + expireAfterWriteDuration = 1.hours 48 + } 49 + 50 /** 51 * Resolve a handle's DID. 52 * ··· 61 */ 62 suspend fun resolveDidFromHandle(handle: String): Did { 63 log.debug("Resolving DID for '{}'...", handle) 64 + 65 + // Return cached DID if it exists. 66 + handleDidCache.get(handle)?.let { did -> 67 + log.debug("Returning cached DID for '{}': {}", handle, did.toString()) 68 + return did 69 + } 70 71 @Serializable 72 data class ResolveHandleResponse(val did: String) ··· 122 else -> throw RuntimeException("Received unexpected response: ${resolveHandleResponse.bodyAsText()}") 123 } 124 125 + // Cache the DID for this handle. 126 + handleDidCache.put(handle, did) 127 + log.debug("Resolved and cached DID for '{}': {}", handle, did.toString()) 128 129 return did 130 } ··· 143 */ 144 suspend fun resolvePdsFromDid(did: Did): String { 145 log.debug("Resolving PDS for '{}'...", did.toString()) 146 + 147 + // Return cached PDS host if it exists. 148 + didPdsCache.get(did)?.let { pdsHost -> 149 + log.debug("Returning cached PDS host for '{}': {}", did.toString(), pdsHost) 150 + return pdsHost 151 + } 152 153 // Get the DID document, using either `plc.directory` or 154 // `/.well-known/did.json` depending on the type of DID. ··· 195 ?.serviceEndpoint 196 ?: throw PdsNotFoundException(did) 197 198 + // Cache the PDS for this DID. 199 + didPdsCache.put(did, pdsHost) 200 + log.debug("Resolved and cached PDS for '{}': {}", did.toString(), pdsHost) 201 202 return pdsHost 203 + } 204 + 205 + /** 206 + * Invalidate the PDS associated with the given DID. 207 + * 208 + * This should be used when a call to the PDS fails due to the PDS no longer 209 + * being available. This may indicate that the PDS has moved and should be 210 + * resolved again. 211 + * 212 + * @param did DID to invalidate. 213 + */ 214 + suspend fun invalidateCachedPds(did: Did) { 215 + didPdsCache.remove(did) 216 } 217 } 218
+2
darkfeed/src/main/kotlin/api/BskyApi.kt
··· 92 val record: Generator, 93 ) 94 95 val ownerPdsHost = atpIdentityClient.resolvePdsFromDid(authManager.authAccountDid.toDid()) 96 97 val response = httpClient.post { ··· 116 @Serializable 117 data class Response(val cursor: String?, val records: List<LikeRef>) 118 119 val requestorPdsHost = atpIdentityClient.resolvePdsFromDid(actor.toDid()) 120 121 val response = httpClient.get {
··· 92 val record: Generator, 93 ) 94 95 + // TODO: If this PDS is not accessible, invalidate cache and retry. 96 val ownerPdsHost = atpIdentityClient.resolvePdsFromDid(authManager.authAccountDid.toDid()) 97 98 val response = httpClient.post { ··· 117 @Serializable 118 data class Response(val cursor: String?, val records: List<LikeRef>) 119 120 + // TODO: If this PDS is not accessible, invalidate cache and retry. 121 val requestorPdsHost = atpIdentityClient.resolvePdsFromDid(actor.toDid()) 122 123 val response = httpClient.get {
+2
gradle/libs.versions.toml
··· 4 ktor = "3.0.1" 5 auth0 = "4.4.0" 6 clikt = "5.0.1" 7 8 [libraries] 9 logback-classic = { group = "ch.qos.logback", name = "logback-classic", version.ref = "logback" } ··· 18 ktor-serialization-kotlinx-json = { group = "io.ktor", name = "ktor-serialization-kotlinx-json", version.ref = "ktor" } 19 java-jwt = { group = "com.auth0", name = "java-jwt", version.ref = "auth0" } 20 clikt = { group = "com.github.ajalt.clikt", name = "clikt", version.ref = "clikt" } 21 22 [plugins] 23 kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
··· 4 ktor = "3.0.1" 5 auth0 = "4.4.0" 6 clikt = "5.0.1" 7 + kache = "2.1.1" 8 9 [libraries] 10 logback-classic = { group = "ch.qos.logback", name = "logback-classic", version.ref = "logback" } ··· 19 ktor-serialization-kotlinx-json = { group = "io.ktor", name = "ktor-serialization-kotlinx-json", version.ref = "ktor" } 20 java-jwt = { group = "com.auth0", name = "java-jwt", version.ref = "auth0" } 21 clikt = { group = "com.github.ajalt.clikt", name = "clikt", version.ref = "clikt" } 22 + kache = { group = "com.mayakapps.kache", name = "kache", version.ref = "kache" } 23 24 [plugins] 25 kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }