Bluesky feed server - NSFW Likes
1
fork

Configure Feed

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

wip: Show feed of liked posts

No filtering yet.

+759 -2
+11
app/build.gradle.kts
··· 1 1 plugins { 2 2 alias(libs.plugins.kotlin.jvm) 3 + alias(libs.plugins.kotlin.serialization) 3 4 application 4 5 } 5 6 ··· 8 9 } 9 10 10 11 dependencies { 12 + implementation(libs.logback.classic) 13 + implementation(libs.ktor.client.core) 14 + implementation(libs.ktor.client.cio) 15 + implementation(libs.ktor.client.content.negotiation) 16 + implementation(libs.ktor.client.logging) 17 + implementation(libs.ktor.server.core) 18 + implementation(libs.ktor.server.netty) 19 + implementation(libs.ktor.server.content.negotiation) 20 + implementation(libs.ktor.serialization.kotlinx.json) 11 21 } 12 22 13 23 java { ··· 18 28 19 29 application { 20 30 mainClass = "gay.averyrivers.ApplicationKt" 31 + applicationDefaultJvmArgs = listOf("-Dio.ktor.development=true") 21 32 }
+370 -1
app/src/main/kotlin/Application.kt
··· 1 1 package gay.averyrivers 2 2 3 + import gay.averyrivers.lexicon.app.bsky.feed.Generator 4 + import gay.averyrivers.lexicon.app.bsky.feed.Like 5 + import gay.averyrivers.lexicon.app.bsky.feed.Post 6 + import io.ktor.client.* 7 + import io.ktor.client.call.* 8 + import io.ktor.client.engine.cio.* 9 + import io.ktor.client.request.* 10 + import io.ktor.client.statement.* 11 + import io.ktor.http.* 12 + import io.ktor.serialization.kotlinx.json.* 13 + import io.ktor.server.application.* 14 + import io.ktor.server.engine.* 15 + import io.ktor.server.netty.* 16 + import io.ktor.server.plugins.contentnegotiation.* 17 + import io.ktor.server.response.* 18 + import io.ktor.server.routing.* 19 + import io.ktor.server.util.* 20 + import kotlinx.coroutines.runBlocking 21 + import kotlinx.serialization.Contextual 22 + import kotlinx.serialization.SerialName 23 + import kotlinx.serialization.Serializable 24 + import kotlinx.serialization.json.Json 25 + import java.util.logging.Logger 26 + 27 + const val FEED_DISPLAY_NAME = "DarkFeed" 28 + const val FEED_DESCRIPTION = "Hi!" 29 + const val FEED_RECORD_KEY = "darkfeed" 30 + 3 31 fun main() { 4 - println("Hello, world!") 32 + // TODO: Move this to an env file! 33 + val hostname = "" 34 + val ownerPds = "" 35 + val ownerDid = "" 36 + val ownerAppPassword = "" 37 + 38 + val api = BskyApi( 39 + pdsUrl = buildUrl { 40 + protocol = URLProtocol.HTTPS 41 + host = ownerPds 42 + } 43 + ) 44 + 45 + // Make sure the feed generator record exists and points to the current 46 + // feed generator's hostname. 47 + runBlocking { 48 + api.login(ownerDid, ownerAppPassword) 49 + 50 + try { 51 + verifyAndUpdateFeedGeneratorRecord(api, ownerDid, FEED_RECORD_KEY, hostname) 52 + println("Successfully set feed generator record") 53 + } catch (error: Exception) { 54 + println("Failed to verify and update feed generator record: ${error.message}") 55 + } 56 + } 57 + 58 + // Serve the feed generator API. 59 + DarkFeedApi( 60 + hostname = hostname, 61 + bskyApi = api, 62 + port = 8080, 63 + ).serve() 5 64 } 65 + 66 + /** 67 + * Verify the current feed generator record, creating or updating it if necessary. 68 + * 69 + * @param api Bluesky API instance. Requires login. 70 + * @param repo Owner of the record. 71 + * @param rkey Record key of the record to check. 72 + * @param labelerHostname Hostname of the feed generator. 73 + */ 74 + suspend fun verifyAndUpdateFeedGeneratorRecord(api: BskyApi, repo: String, rkey: String, labelerHostname: String) { 75 + // Get the current record. 76 + var feedGeneratorRecord = api.getFeedGeneratorRecord(repo, rkey) 77 + 78 + // If the current record exists and has the correct DID, nothing needs to be done. 79 + if (feedGeneratorRecord?.did?.contains(labelerHostname) == true) return 80 + 81 + // Update the current record if one exists, or create a new one if it doesn't. 82 + feedGeneratorRecord = feedGeneratorRecord 83 + ?.copy(did = "did:web:$labelerHostname") 84 + ?: Generator( 85 + did = "did:web:$labelerHostname", 86 + displayName = FEED_DISPLAY_NAME, 87 + description = FEED_DESCRIPTION, 88 + createdAt = "2024-11-04T15:58:05.074Z", 89 + ) 90 + 91 + // Store the new/updated record in the repo. 92 + api.putFeedGeneratorRecord(repo, rkey, feedGeneratorRecord) 93 + } 94 + 95 + //@Serializable 96 + //data class DidDoc( 97 + // val id: String, 98 + // val service: List<Service>, 99 + //) { 100 + // @Serializable 101 + // data class Service( 102 + // val id: String, 103 + // val type: String, 104 + // val serviceEndpoint: String, 105 + // ) 106 + //} 107 + // 108 + //@Serializable 109 + //data class FeedSkeleton( 110 + // val cursor: String? = null, 111 + // val feed: List<FeedObject>, 112 + //) { 113 + // @Serializable 114 + // data class FeedObject( 115 + // val post: String, 116 + // val reason: ReasonRepost? = null, 117 + // val feedContext: String? = null, 118 + // ) { 119 + // @Serializable 120 + // data class ReasonRepost( 121 + // val repost: String, 122 + // ) 123 + // } 124 + //} 125 + // 126 + //@Serializable 127 + //data class AppBskyFeedGenerator( 128 + // val did: String, 129 + // @SerialName("\$type") 130 + // val type: String? = null, 131 + // val displayName: String? = null, 132 + // val createdAt: String? = null, 133 + //) 134 + // 135 + //fun main() { 136 + // val hostname = "" 137 + // val ownerPds = "" 138 + // val ownerDid = "" 139 + // val ownerAppPassword = "" 140 + // val feedRecordKey = "" 141 + // 142 + // val client = HttpClient(CIO) { 143 + // install(io.ktor.client.plugins.contentnegotiation.ContentNegotiation) { 144 + // json( 145 + // Json { 146 + // explicitNulls = false 147 + // ignoreUnknownKeys = true 148 + // } 149 + // ) 150 + // } 151 + // } 152 + // 153 + // runBlocking { 154 + // val getRecordResponse = client.get("https://$ownerPds/xrpc/com.atproto.repo.getRecord") { 155 + // url { 156 + // parameters.append("repo", ownerDid) 157 + // parameters.append("collection", "app.bsky.feed.generator") 158 + // parameters.append("rkey", feedRecordKey) 159 + // } 160 + // } 161 + // 162 + // @Serializable 163 + // data class GetRecordResponse( 164 + // val uri: String, 165 + // val cid: String?, 166 + // val value: AppBskyFeedGenerator, 167 + // ) 168 + // 169 + //// val appBskyFeedGeneratorRecord: GetRecordResponse = getRecordResponse.body() 170 + // 171 + // val appBskyFeedGeneratorRecord: GetRecordResponse? = if (getRecordResponse.status == HttpStatusCode.OK) { 172 + // getRecordResponse.body() 173 + // } else { 174 + // null 175 + // } 176 + // 177 + // if (appBskyFeedGeneratorRecord?.value?.did?.contains(hostname) == true) { 178 + // return@runBlocking 179 + // } 180 + // 181 + // println("Updating app.bsky.feed.record did with new hostname $hostname") 182 + // 183 + // @Serializable 184 + // data class CreateSessionRequest( 185 + // val identifier: String, 186 + // val password: String, 187 + // ) 188 + // 189 + // @Serializable 190 + // data class CreateSessionResponse( 191 + // val accessJwt: String, 192 + // val refreshJwt: String, 193 + // val did: String, 194 + // ) 195 + // 196 + // val sessionTokens: CreateSessionResponse = 197 + // client.post("https://$ownerPds/xrpc/com.atproto.server.createSession") { 198 + // contentType(ContentType.Application.Json) 199 + // setBody( 200 + // CreateSessionRequest( 201 + // identifier = ownerDid, 202 + // password = ownerAppPassword, 203 + // ) 204 + // ) 205 + // }.body() 206 + // 207 + // 208 + // if (sessionTokens.did != ownerDid) { 209 + // println("How is this possible!? :O") 210 + // return@runBlocking 211 + // } 212 + // 213 + // @Serializable 214 + // data class PutRecordRequest( 215 + // val repo: String, 216 + // val collection: String, 217 + // val rkey: String, 218 + // val validate: Boolean? = null, 219 + // val record: AppBskyFeedGenerator, 220 + // val swapRecord: String? = null, 221 + // val swapCommit: String? = null, 222 + // ) 223 + // 224 + // val putRecordResponse = client.post("https://$ownerPds/xrpc/com.atproto.repo.putRecord") { 225 + // header("Authorization", "Bearer ${sessionTokens.accessJwt}") 226 + // contentType(ContentType.Application.Json) 227 + // setBody( 228 + // PutRecordRequest( 229 + // repo = ownerDid, 230 + // collection = "app.bsky.feed.generator", 231 + // rkey = feedRecordKey, 232 + // record = AppBskyFeedGenerator( 233 + // did = "did:web:$hostname", 234 + // displayName = "DarkFeed", 235 + // createdAt = "2024-11-04T15:58:05.074Z", 236 + // ) 237 + // ) 238 + // ) 239 + // } 240 + // 241 + // if (putRecordResponse.status != HttpStatusCode.OK) { 242 + // println("Failed to update hostname in feed generator record: ${putRecordResponse.bodyAsText()}") 243 + // } 244 + // } 245 + // 246 + // // Run DarkFeed server. 247 + // embeddedServer(Netty, port = 8080) { 248 + // install(ContentNegotiation) { 249 + // json(Json { explicitNulls = false }) 250 + // } 251 + // 252 + // routing { 253 + // get("/.well-known/did.json") { 254 + // call.respond( 255 + // DidDoc( 256 + // id = "did:web:$hostname", 257 + // service = listOf( 258 + // DidDoc.Service( 259 + // id = "#bsky_fg", 260 + // type = "BskyFeedGenerator", 261 + // serviceEndpoint = "https://$hostname", 262 + // ) 263 + // ) 264 + // ) 265 + // ) 266 + // } 267 + // 268 + // get("/xrpc/app.bsky.feed.getFeedSkeleton") { 269 + // call.respond(buildFeedSkeleton(actor = ownerDid)) 270 + // } 271 + // } 272 + // }.start(wait = true) 273 + //} 274 + // 275 + //// SOOOOO 276 + //// I NEED TO CHECK THE SELF LABEL (at://did:plc:vwivwqztbf6pmkgss3nv2scy/app.bsky.feed.post/3la5sxh4ica2r) 277 + //// AND THE MODERATION.BSKY.APP LABEL (at://did:plc:ujrpupcjf22a4riwjbupdv42/app.bsky.feed.post/3la5qntezsu2v) 278 + //suspend fun buildFeedSkeleton(actor: String) = FeedSkeleton( 279 + // feed = getActorLikes(actor = actor) 280 + // .map { FeedSkeleton.FeedObject(post = it) } 281 + //) 282 + // 283 + //suspend fun getPostLabels(uris: List<String>): Unit { 284 + // val client = HttpClient(CIO) { 285 + // install(io.ktor.client.plugins.contentnegotiation.ContentNegotiation) { 286 + // json( 287 + // Json { 288 + // explicitNulls = false 289 + // ignoreUnknownKeys = true 290 + // } 291 + // ) 292 + // } 293 + // } 294 + // 295 + // @Serializable 296 + // data class GetPostsRequest( 297 + // val uris: List<String>, 298 + // ) 299 + // 300 + // @Serializable 301 + // data class Label( 302 + // val ver: Int, 303 + // val src: String, 304 + // val uri: String, 305 + // val cid: String, 306 + // @SerialName("val") 307 + // val _val: String, 308 + // val cts: String, 309 + // ) 310 + // 311 + // @Serializable 312 + // data class HydratedPostView( 313 + // val uri: String, 314 + // val cid: String, 315 + // val labels: List<Label>, 316 + // ) 317 + // 318 + // @Serializable 319 + // data class GetPostsResponse( 320 + // val posts: List<HydratedPostView>, 321 + // ) 322 + // 323 + // val getPostsResponse = client.get("https://bsky.social/xrpc/app.bsky.geed.getPosts") { 324 + // parameters { 325 + // uris.forEach { uri -> parameter("uris", uri) } 326 + // } 327 + // } 328 + // 329 + // println(getPostsResponse.bodyAsText()) 330 + // 331 + // val posts: GetPostsResponse = getPostsResponse.body() 332 + // 333 + // println(getPostsResponse) 334 + //} 335 + // 336 + //suspend fun getActorLikes(actor: String, cursor: String? = null): List<String> { 337 + // val client = HttpClient(CIO) { 338 + // install(io.ktor.client.plugins.contentnegotiation.ContentNegotiation) { 339 + // json( 340 + // Json { 341 + // explicitNulls = false 342 + // ignoreUnknownKeys = true 343 + // } 344 + // ) 345 + // } 346 + // } 347 + // 348 + // @Serializable 349 + // data class RefRecord( 350 + // val uri: String, 351 + // val cid: String, 352 + // val value: Like, 353 + // ) 354 + // 355 + // @Serializable 356 + // data class ListRecordsResponse( 357 + // val cursor: String?, 358 + // val records: List<RefRecord>, 359 + // ) 360 + // 361 + // val response = client.get("https://bsky.social/xrpc/com.atproto.repo.listRecords") { 362 + // parameters { 363 + // parameter("repo", actor) 364 + // parameter("collection", "app.bsky.feed.like") 365 + // parameter("limit", 100) 366 + // if (cursor != null) parameter("cursor", cursor) 367 + // } 368 + // } 369 + // 370 + // return response.body<ListRecordsResponse>() 371 + // .records 372 + // .map { it.value.subject.uri } 373 + // .toList() 374 + //}
+178
app/src/main/kotlin/BskyApi.kt
··· 1 + package gay.averyrivers 2 + 3 + import gay.averyrivers.lexicon.app.bsky.feed.Generator 4 + import gay.averyrivers.lexicon.app.bsky.feed.LikeRef 5 + import gay.averyrivers.lexicon.app.bsky.feed.defs.PostView 6 + import gay.averyrivers.lexicon.com.atproto.label.defs.Label 7 + import io.ktor.client.* 8 + import io.ktor.client.call.* 9 + import io.ktor.client.engine.cio.* 10 + import io.ktor.client.plugins.* 11 + import io.ktor.client.plugins.contentnegotiation.* 12 + import io.ktor.client.plugins.logging.* 13 + import io.ktor.client.request.* 14 + import io.ktor.client.statement.* 15 + import io.ktor.http.* 16 + import io.ktor.serialization.kotlinx.json.* 17 + import kotlinx.serialization.Serializable 18 + import kotlinx.serialization.json.Json 19 + 20 + class BskyApi( 21 + private var pdsUrl: Url = Url("https://bsky.social"), 22 + 23 + private val httpClient: HttpClient = HttpClient(CIO) { 24 + install(ContentNegotiation) { 25 + json(Json { 26 + explicitNulls = false 27 + ignoreUnknownKeys = true 28 + }) 29 + } 30 + 31 + install(Logging) 32 + 33 + defaultRequest { 34 + url { 35 + protocol = pdsUrl.protocol 36 + host = pdsUrl.host 37 + path("xrpc/") 38 + } 39 + } 40 + }, 41 + ) { 42 + 43 + 44 + data class AuthTokens( 45 + val accessJwt: String, 46 + val refreshJwt: String, 47 + val did: String, 48 + ) 49 + 50 + private var authTokens: AuthTokens? = null 51 + 52 + @Serializable 53 + data class ErrorResponse( 54 + val error: String, 55 + val message: String, 56 + ) 57 + 58 + suspend fun login(identifier: String, password: String) { 59 + @Serializable 60 + data class Request(val identifier: String, val password: String) 61 + 62 + @Serializable 63 + data class Response(val did: String, val accessJwt: String, val refreshJwt: String) 64 + 65 + val response = httpClient.post("com.atproto.server.createSession") { 66 + contentType(ContentType.Application.Json) 67 + setBody(Request(identifier, password)) 68 + } 69 + 70 + when (response.status) { 71 + HttpStatusCode.OK -> { 72 + val tokens: Response = response.body() 73 + this.authTokens = AuthTokens(tokens.accessJwt, tokens.refreshJwt, tokens.did) 74 + } 75 + 76 + HttpStatusCode.BadRequest, 77 + HttpStatusCode.Unauthorized -> throw RuntimeException("Failed to create session: ${response.bodyAsText()}") 78 + 79 + else -> throw RuntimeException("Unexpected response received: ${response.bodyAsText()}") 80 + } 81 + } 82 + 83 + suspend fun getFeedGeneratorRecord(repo: String, rkey: String): Generator? { 84 + @Serializable 85 + data class Response(val uri: String, val value: Generator) 86 + 87 + val response = httpClient.get("com.atproto.repo.getRecord") { 88 + parameter("repo", repo) 89 + parameter("collection", "app.bsky.feed.generator") 90 + parameter("rkey", rkey) 91 + } 92 + 93 + when (response.status) { 94 + HttpStatusCode.OK -> { 95 + val record: Response = response.body() 96 + return record.value 97 + } 98 + 99 + HttpStatusCode.BadRequest -> if (response.body<ErrorResponse>().error == "RecordNotFound") return null 100 + 101 + HttpStatusCode.Unauthorized -> throw RuntimeException("Failed to get record: ${response.bodyAsText()}") 102 + 103 + else -> throw RuntimeException("Unexpected response received: ${response.bodyAsText()}") 104 + } 105 + 106 + // Why is this needed? 107 + return null 108 + } 109 + 110 + suspend fun putFeedGeneratorRecord(repo: String, rkey: String, record: Generator) { 111 + @Serializable 112 + data class Request( 113 + val repo: String, 114 + val rkey: String, 115 + val record: Generator, 116 + val collection: String = "app.bsky.feed.generator", 117 + ) 118 + 119 + val response = httpClient.post("com.atproto.repo.putRecord") { 120 + contentType(ContentType.Application.Json) 121 + setBody(Request(repo, rkey, record)) 122 + } 123 + 124 + when (response.status) { 125 + HttpStatusCode.BadRequest, 126 + HttpStatusCode.Unauthorized -> throw RuntimeException("Failed to put record: ${response.bodyAsText()}") 127 + } 128 + } 129 + 130 + suspend fun getLikesByActor(actor: String, cursor: String? = null): Pair<List<LikeRef>, String?> { 131 + @Serializable 132 + data class Response(val cursor: String?, val records: List<LikeRef>) 133 + 134 + val response = httpClient.get("com.atproto.repo.listRecords") { 135 + parameter("repo", actor) 136 + parameter("collection", "app.bsky.feed.like") 137 + parameter("limit", 100) 138 + if (cursor != null) parameter("cursor", cursor) 139 + } 140 + 141 + when (response.status) { 142 + HttpStatusCode.OK -> { 143 + val response: Response = response.body() 144 + 145 + val likeRefs = response.records 146 + val cursor = response.cursor 147 + 148 + return Pair(likeRefs, cursor) 149 + } 150 + 151 + HttpStatusCode.BadRequest, 152 + HttpStatusCode.Unauthorized -> throw RuntimeException("Failed to get likes: ${response.bodyAsText()}") 153 + 154 + else -> throw RuntimeException("Unexpected response received: ${response.bodyAsText()}") 155 + } 156 + } 157 + 158 + suspend fun getPostLabels(posts: List<String>): List<PostView> { 159 + @Serializable 160 + data class Response(val posts: List<PostView>) 161 + 162 + val response = httpClient.get("app.bsky.feed.getPosts") { 163 + posts.forEach { parameter("uris", it) } 164 + } 165 + 166 + when (response.status) { 167 + HttpStatusCode.OK -> { 168 + val response: Response = response.body() 169 + return response.posts 170 + } 171 + 172 + HttpStatusCode.BadRequest, 173 + HttpStatusCode.Unauthorized -> throw RuntimeException("Failed to get posts: ${response.bodyAsText()}") 174 + 175 + else -> throw RuntimeException("Unexpected response received: ${response.bodyAsText()}") 176 + } 177 + } 178 + }
+83
app/src/main/kotlin/DarkFeedApi.kt
··· 1 + package gay.averyrivers 2 + 3 + import gay.averyrivers.lexicon.app.bsky.feed.FeedSkeleton 4 + import gay.averyrivers.lexicon.app.bsky.feed.defs.SkeletonFeedPost 5 + import io.ktor.serialization.kotlinx.json.* 6 + import io.ktor.server.application.* 7 + import io.ktor.server.engine.* 8 + import io.ktor.server.netty.* 9 + import io.ktor.server.plugins.contentnegotiation.* 10 + import io.ktor.server.response.* 11 + import io.ktor.server.routing.* 12 + import kotlinx.serialization.Serializable 13 + import kotlinx.serialization.json.Json 14 + 15 + 16 + class DarkFeedApi( 17 + private val hostname: String, 18 + private val bskyApi: BskyApi, 19 + private val port: Int = 8080, 20 + ) { 21 + @Serializable 22 + data class DidJson( 23 + val id: String, 24 + val service: List<Service>, 25 + ) { 26 + @Serializable 27 + data class Service( 28 + val id: String, 29 + val type: String, 30 + val serviceEndpoint: String, 31 + ) 32 + } 33 + 34 + // you better work, bitch 35 + fun serve() { 36 + embeddedServer(Netty, port = port) { 37 + install(ContentNegotiation) { 38 + json(Json { 39 + explicitNulls = false 40 + ignoreUnknownKeys = true 41 + }) 42 + } 43 + 44 + routing { 45 + get("/.well-known/did.json") { 46 + handleWellKnownDidJson(call) 47 + } 48 + 49 + get("/xrpc/app.bsky.feed.getFeedSkeleton") { 50 + handleGetFeedSkeleton(call) 51 + } 52 + } 53 + }.start(wait = true) 54 + } 55 + 56 + private suspend fun handleWellKnownDidJson(call: RoutingCall) { 57 + call.respond( 58 + DidJson( 59 + id = "did:web:$hostname", 60 + service = listOf( 61 + DidJson.Service( 62 + id = "#bsky_fg", 63 + type = "BskyFeedGenerator", 64 + serviceEndpoint = "https://$hostname", 65 + ) 66 + ) 67 + ) 68 + ) 69 + } 70 + 71 + private suspend fun handleGetFeedSkeleton(call: RoutingCall) { 72 + // TODO: Get requestor's DID from Authorization header. 73 + call.respond(buildFeedSkeleton("did:plc:zhxv5pxpmojhnvaqy4mwailv")) 74 + } 75 + 76 + private suspend fun buildFeedSkeleton(requestor: String): FeedSkeleton { 77 + return FeedSkeleton( 78 + feed = bskyApi.getLikesByActor(requestor) 79 + .first 80 + .map { likeRef -> SkeletonFeedPost(post = likeRef.value.subject.uri) } 81 + ) 82 + } 83 + }
+10
app/src/main/kotlin/lexicon/app/bsky/feed/FeedSkeleton.kt
··· 1 + package gay.averyrivers.lexicon.app.bsky.feed 2 + 3 + import gay.averyrivers.lexicon.app.bsky.feed.defs.SkeletonFeedPost 4 + import kotlinx.serialization.Serializable 5 + 6 + @Serializable 7 + data class FeedSkeleton( 8 + val cursor: String? = null, 9 + val feed: List<SkeletonFeedPost>, 10 + )
+11
app/src/main/kotlin/lexicon/app/bsky/feed/Generator.kt
··· 1 + package gay.averyrivers.lexicon.app.bsky.feed 2 + 3 + import kotlinx.serialization.Serializable 4 + 5 + @Serializable 6 + data class Generator( 7 + val did: String, 8 + val displayName: String, 9 + val description: String?, 10 + val createdAt: String, 11 + )
+17
app/src/main/kotlin/lexicon/app/bsky/feed/Like.kt
··· 1 + package gay.averyrivers.lexicon.app.bsky.feed 2 + 3 + import gay.averyrivers.lexicon.com.atproto.repo.StrongRef 4 + import kotlinx.serialization.Serializable 5 + 6 + @Serializable 7 + data class Like( 8 + val subject: StrongRef, 9 + val createdAt: String, 10 + ) 11 + 12 + @Serializable 13 + data class LikeRef( 14 + val uri: String, 15 + val cid: String, 16 + val value: Like, 17 + )
+9
app/src/main/kotlin/lexicon/app/bsky/feed/Post.kt
··· 1 + package gay.averyrivers.lexicon.app.bsky.feed 2 + 3 + import kotlinx.serialization.Serializable 4 + 5 + @Serializable 6 + data class Post( 7 + val text: String, 8 + val createdAt: String, 9 + )
+11
app/src/main/kotlin/lexicon/app/bsky/feed/defs/PostView.kt
··· 1 + package gay.averyrivers.lexicon.app.bsky.feed.defs 2 + 3 + import gay.averyrivers.lexicon.com.atproto.label.defs.Label 4 + import kotlinx.serialization.Serializable 5 + 6 + @Serializable 7 + data class PostView( 8 + val uri: String, 9 + val cid: String, 10 + val labels: List<Label>, 11 + )
+9
app/src/main/kotlin/lexicon/app/bsky/feed/defs/SkeletonFeedPost.kt
··· 1 + package gay.averyrivers.lexicon.app.bsky.feed.defs 2 + 3 + import kotlinx.serialization.Serializable 4 + 5 + @Serializable 6 + data class SkeletonFeedPost( 7 + val post: String, 8 + val feedContext: String? = null, 9 + )
+15
app/src/main/kotlin/lexicon/com/atproto/label/defs/Label.kt
··· 1 + package gay.averyrivers.lexicon.com.atproto.label.defs 2 + 3 + import kotlinx.serialization.Serializable 4 + 5 + @Serializable 6 + data class Label( 7 + val ver: Int?, 8 + val src: String, 9 + val uri: String, 10 + val cid: String?, 11 + val value: String, 12 + val neg: Boolean?, 13 + val cts: String, 14 + val exp: String?, 15 + )
+9
app/src/main/kotlin/lexicon/com/atproto/repo/StrongRef.kt
··· 1 + package gay.averyrivers.lexicon.com.atproto.repo 2 + 3 + import kotlinx.serialization.Serializable 4 + 5 + @Serializable 6 + data class StrongRef( 7 + val uri: String, 8 + val cid: String, 9 + )
+12
app/src/main/resources/logback.xml
··· 1 + <configuration> 2 + <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> 3 + <encoder> 4 + <pattern>%d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern> 5 + </encoder> 6 + </appender> 7 + <root level="trace"> 8 + <appender-ref ref="STDOUT"/> 9 + </root> 10 + <logger name="io.netty" level="INFO"/> 11 + <logger name="io.ktor" level="TRACE"/> 12 + </configuration>
+14 -1
gradle/libs.versions.toml
··· 1 1 [versions] 2 + kotlin = "2.0.21" 3 + logback = "1.5.12" 4 + ktor = "3.0.1" 2 5 3 6 [libraries] 7 + logback-classic = { group = "ch.qos.logback", name = "logback-classic", version.ref = "logback" } 8 + ktor-client-core = { group = "io.ktor", name = "ktor-client-core", version.ref = "ktor" } 9 + ktor-client-cio = { group = "io.ktor", name = "ktor-client-cio", version.ref = "ktor" } 10 + ktor-client-content-negotiation = { group = "io.ktor", name = "ktor-client-content-negotiation", version.ref = "ktor" } 11 + ktor-client-logging = { group = "io.ktor", name = "ktor-client-logging", version.ref = "ktor" } 12 + ktor-server-core = { group = "io.ktor", name = "ktor-server-core", version.ref = "ktor" } 13 + ktor-server-netty = { group = "io.ktor", name = "ktor-server-netty", version.ref = "ktor" } 14 + ktor-server-content-negotiation = { group = "io.ktor", name = "ktor-server-content-negotiation", version.ref = "ktor" } 15 + ktor-serialization-kotlinx-json = { group = "io.ktor", name = "ktor-serialization-kotlinx-json", version.ref = "ktor" } 4 16 5 17 [plugins] 6 - kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version = "2.0.0" } 18 + kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } 19 + kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }