+1
app/build.gradle.kts
+1
app/build.gradle.kts
···
14
14
implementation(libs.ktor.client.cio)
15
15
implementation(libs.ktor.client.content.negotiation)
16
16
implementation(libs.ktor.client.logging)
17
+
implementation(libs.ktor.client.auth)
17
18
implementation(libs.ktor.server.core)
18
19
implementation(libs.ktor.server.netty)
19
20
implementation(libs.ktor.server.content.negotiation)
+1
-1
app/src/main/kotlin/Application.kt
+1
-1
app/src/main/kotlin/Application.kt
+46
-15
app/src/main/kotlin/BskyApi.kt
+46
-15
app/src/main/kotlin/BskyApi.kt
···
8
8
import io.ktor.client.call.*
9
9
import io.ktor.client.engine.cio.*
10
10
import io.ktor.client.plugins.*
11
+
import io.ktor.client.plugins.auth.*
12
+
import io.ktor.client.plugins.auth.providers.*
11
13
import io.ktor.client.plugins.contentnegotiation.*
12
14
import io.ktor.client.plugins.logging.*
13
15
import io.ktor.client.request.*
···
18
20
import kotlinx.serialization.json.Json
19
21
20
22
class BskyApi(
21
-
private var pdsUrl: Url = Url("https://bsky.social"),
23
+
private val pdsUrl: Url = Url("https://bsky.social"),
24
+
25
+
private val bearerTokens: MutableList<BearerTokens> = mutableListOf(),
22
26
23
27
private val httpClient: HttpClient = HttpClient(CIO) {
24
28
install(ContentNegotiation) {
···
30
34
31
35
install(Logging)
32
36
37
+
install(Auth) {
38
+
bearer {
39
+
loadTokens {
40
+
bearerTokens.lastOrNull()
41
+
}
42
+
43
+
refreshTokens {
44
+
val currentRefreshToken = bearerTokens.lastOrNull()?.refreshToken ?: return@refreshTokens null
45
+
46
+
@Serializable
47
+
data class Response(val accessJwt: String, val refreshJwt: String)
48
+
49
+
val refreshSessionResponse = client.post("com.atproto.server.refreshSession") {
50
+
header("Authorization", "Bearer $currentRefreshToken")
51
+
markAsRefreshTokenRequest()
52
+
}
53
+
54
+
when (refreshSessionResponse.status) {
55
+
HttpStatusCode.OK -> {
56
+
val refreshSessionTokens = refreshSessionResponse.body<Response>()
57
+
val newBearerTokens =
58
+
BearerTokens(refreshSessionTokens.accessJwt, refreshSessionTokens.refreshJwt)
59
+
60
+
bearerTokens.addLast(newBearerTokens)
61
+
62
+
return@refreshTokens newBearerTokens
63
+
}
64
+
65
+
HttpStatusCode.BadRequest,
66
+
HttpStatusCode.Unauthorized -> return@refreshTokens null
67
+
68
+
else -> return@refreshTokens null
69
+
}
70
+
}
71
+
}
72
+
}
73
+
33
74
defaultRequest {
34
75
url {
35
76
protocol = pdsUrl.protocol
···
39
80
}
40
81
},
41
82
) {
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
83
@Serializable
53
84
data class ErrorResponse(
54
85
val error: String,
···
70
101
when (response.status) {
71
102
HttpStatusCode.OK -> {
72
103
val tokens: Response = response.body()
73
-
this.authTokens = AuthTokens(tokens.accessJwt, tokens.refreshJwt, tokens.did)
104
+
bearerTokens.addLast(BearerTokens(tokens.accessJwt, tokens.refreshJwt))
74
105
}
75
106
76
107
HttpStatusCode.BadRequest,
···
111
142
@Serializable
112
143
data class Request(
113
144
val repo: String,
145
+
val collection: String,
114
146
val rkey: String,
115
147
val record: Generator,
116
-
val collection: String = "app.bsky.feed.generator",
117
148
)
118
149
119
150
val response = httpClient.post("com.atproto.repo.putRecord") {
120
151
contentType(ContentType.Application.Json)
121
-
setBody(Request(repo, rkey, record))
152
+
setBody(Request(repo, "app.bsky.feed.generator", rkey, record))
122
153
}
123
154
124
155
when (response.status) {
···
175
206
else -> throw RuntimeException("Unexpected response received: ${response.bodyAsText()}")
176
207
}
177
208
}
178
-
}
209
+
}
+18
-8
app/src/main/kotlin/DarkFeedApi.kt
+18
-8
app/src/main/kotlin/DarkFeedApi.kt
···
1
1
package gay.averyrivers
2
2
3
3
import gay.averyrivers.lexicon.app.bsky.feed.FeedSkeleton
4
+
import gay.averyrivers.lexicon.app.bsky.feed.defs.PostView
4
5
import gay.averyrivers.lexicon.app.bsky.feed.defs.SkeletonFeedPost
5
6
import io.ktor.serialization.kotlinx.json.*
6
7
import io.ktor.server.application.*
···
68
69
)
69
70
}
70
71
71
-
// SOOOOO
72
-
// I NEED TO CHECK THE SELF LABEL (at://did:plc:vwivwqztbf6pmkgss3nv2scy/app.bsky.feed.post/3la5sxh4ica2r)
73
-
// AND THE MODERATION.BSKY.APP LABEL (at://did:plc:ujrpupcjf22a4riwjbupdv42/app.bsky.feed.post/3la5qntezsu2v)
74
-
75
72
private suspend fun handleGetFeedSkeleton(call: RoutingCall) {
76
73
// TODO: Get requestor's DID from Authorization header.
77
74
call.respond(buildFeedSkeleton("did:plc:zhxv5pxpmojhnvaqy4mwailv"))
78
75
}
79
76
80
77
private suspend fun buildFeedSkeleton(requestor: String): FeedSkeleton {
78
+
val actorLikes = bskyApi.getLikesByActor(requestor)
79
+
.first
80
+
.map { likeRef -> likeRef.value.subject.uri }
81
+
82
+
val labeledPosts = actorLikes
83
+
.chunked(25)
84
+
.map { chunkedActorLikes ->
85
+
bskyApi.getPostLabels(chunkedActorLikes)
86
+
.filter { post ->
87
+
post.labels?.any { label -> listOf("porn", "sexual").contains(label.value) } ?: false
88
+
}
89
+
}
90
+
.flatten()
91
+
92
+
81
93
return FeedSkeleton(
82
-
feed = bskyApi.getLikesByActor(requestor)
83
-
.first
84
-
.map { likeRef -> SkeletonFeedPost(post = likeRef.value.subject.uri) }
94
+
feed = labeledPosts.map { post -> SkeletonFeedPost(post = post.uri) }
85
95
)
86
96
}
87
-
}
97
+
}
+2
-2
app/src/main/kotlin/lexicon/app/bsky/feed/defs/PostView.kt
+2
-2
app/src/main/kotlin/lexicon/app/bsky/feed/defs/PostView.kt
+3
-1
app/src/main/kotlin/lexicon/com/atproto/label/defs/Label.kt
+3
-1
app/src/main/kotlin/lexicon/com/atproto/label/defs/Label.kt
···
1
1
package gay.averyrivers.lexicon.com.atproto.label.defs
2
2
3
+
import kotlinx.serialization.SerialName
3
4
import kotlinx.serialization.Serializable
4
5
5
6
@Serializable
···
8
9
val src: String,
9
10
val uri: String,
10
11
val cid: String?,
12
+
@SerialName("val")
11
13
val value: String,
12
14
val neg: Boolean?,
13
15
val cts: String,
14
16
val exp: String?,
15
-
)
17
+
)
+2
-1
app/src/main/resources/logback.xml
+2
-1
app/src/main/resources/logback.xml
+1
gradle/libs.versions.toml
+1
gradle/libs.versions.toml
···
9
9
ktor-client-cio = { group = "io.ktor", name = "ktor-client-cio", version.ref = "ktor" }
10
10
ktor-client-content-negotiation = { group = "io.ktor", name = "ktor-client-content-negotiation", version.ref = "ktor" }
11
11
ktor-client-logging = { group = "io.ktor", name = "ktor-client-logging", version.ref = "ktor" }
12
+
ktor-client-auth = { group = "io.ktor", name = "ktor-client-auth", version.ref = "ktor" }
12
13
ktor-server-core = { group = "io.ktor", name = "ktor-server-core", version.ref = "ktor" }
13
14
ktor-server-netty = { group = "io.ktor", name = "ktor-server-netty", version.ref = "ktor" }
14
15
ktor-server-content-negotiation = { group = "io.ktor", name = "ktor-server-content-negotiation", version.ref = "ktor" }