+2
-2
.fleet/run.json
+2
-2
.fleet/run.json
+3
-3
Containerfile
+3
-3
Containerfile
···
1
1
FROM registry.access.redhat.com/ubi8/openjdk-21 AS build
2
2
WORKDIR /build
3
3
COPY --chown=jboss / .
4
-
RUN ./gradlew --no-daemon :app:installDist
4
+
RUN ./gradlew --no-daemon :darkfeed:installDist
5
5
6
6
FROM registry.access.redhat.com/ubi8/openjdk-21
7
-
COPY --from=build /build/app/build/install/app /app
8
-
ENTRYPOINT /app/bin/app
7
+
COPY --from=build /build/darkfeed/build/install/darkfeed /app
8
+
ENTRYPOINT /app/bin/darkfeed
+1
-2
app/build.gradle.kts
darkfeed/build.gradle.kts
+1
-2
app/build.gradle.kts
darkfeed/build.gradle.kts
-92
app/src/main/kotlin/Application.kt
-92
app/src/main/kotlin/Application.kt
···
1
-
package gay.averyrivers
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
-
31
-
fun main() {
32
-
val ownerPds = System.getenv("DF_OWNER_PDS")
33
-
val ownerDid = System.getenv("DF_OWNER_DID")
34
-
val ownerAppPassword = System.getenv("DF_OWNER_PASSWORD")
35
-
val hostname = System.getenv("DF_HOSTNAME")
36
-
37
-
val api = BskyApi(
38
-
pdsUrl = buildUrl {
39
-
protocol = URLProtocol.HTTPS
40
-
host = ownerPds
41
-
},
42
-
)
43
-
44
-
// Make sure the feed generator record exists and points to the current
45
-
// feed generator's hostname.
46
-
runBlocking {
47
-
api.login(ownerDid, ownerAppPassword)
48
-
49
-
try {
50
-
verifyAndUpdateFeedGeneratorRecord(api, ownerDid, FEED_RECORD_KEY, hostname)
51
-
println("Successfully set feed generator record")
52
-
} catch (error: Exception) {
53
-
println("Failed to verify and update feed generator record: ${error.message}")
54
-
}
55
-
}
56
-
57
-
// Serve the feed generator API.
58
-
DarkFeedApi(
59
-
hostname = hostname,
60
-
bskyApi = api,
61
-
port = 8080,
62
-
).serve()
63
-
}
64
-
65
-
/**
66
-
* Verify the current feed generator record, creating or updating it if necessary.
67
-
*
68
-
* @param api Bluesky API instance. Requires login.
69
-
* @param repo Owner of the record.
70
-
* @param rkey Record key of the record to check.
71
-
* @param labelerHostname Hostname of the feed generator.
72
-
*/
73
-
suspend fun verifyAndUpdateFeedGeneratorRecord(api: BskyApi, repo: String, rkey: String, labelerHostname: String) {
74
-
// Get the current record.
75
-
var feedGeneratorRecord = api.getFeedGeneratorRecord(repo, rkey)
76
-
77
-
// If the current record exists and has the correct DID, nothing needs to be done.
78
-
if (feedGeneratorRecord?.did?.contains(labelerHostname) == true) return
79
-
80
-
// Update the current record if one exists, or create a new one if it doesn't.
81
-
feedGeneratorRecord = feedGeneratorRecord
82
-
?.copy(did = "did:web:$labelerHostname")
83
-
?: Generator(
84
-
did = "did:web:$labelerHostname",
85
-
displayName = FEED_DISPLAY_NAME,
86
-
description = FEED_DESCRIPTION,
87
-
createdAt = "2024-11-04T15:58:05.074Z",
88
-
)
89
-
90
-
// Store the new/updated record in the repo.
91
-
api.putFeedGeneratorRecord(repo, rkey, feedGeneratorRecord)
92
-
}
+4
-5
app/src/main/kotlin/BskyApi.kt
darkfeed/src/main/kotlin/api/BskyApi.kt
+4
-5
app/src/main/kotlin/BskyApi.kt
darkfeed/src/main/kotlin/api/BskyApi.kt
···
1
-
package gay.averyrivers
1
+
package api
2
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
3
+
import api.lexicon.app.bsky.feed.Generator
4
+
import api.lexicon.app.bsky.feed.LikeRef
5
+
import api.lexicon.app.bsky.feed.defs.PostView
7
6
import io.ktor.client.*
8
7
import io.ktor.client.call.*
9
8
import io.ktor.client.engine.cio.*
+6
-5
app/src/main/kotlin/DarkFeedApi.kt
darkfeed/src/main/kotlin/server/FeedServer.kt
+6
-5
app/src/main/kotlin/DarkFeedApi.kt
darkfeed/src/main/kotlin/server/FeedServer.kt
···
1
-
package gay.averyrivers
1
+
package server
2
2
3
+
import api.BskyApi
4
+
import api.lexicon.app.bsky.feed.FeedSkeleton
5
+
import api.lexicon.app.bsky.feed.defs.PostView
6
+
import api.lexicon.app.bsky.feed.defs.SkeletonFeedPost
3
7
import com.auth0.jwt.JWT
4
-
import gay.averyrivers.lexicon.app.bsky.feed.FeedSkeleton
5
-
import gay.averyrivers.lexicon.app.bsky.feed.defs.PostView
6
-
import gay.averyrivers.lexicon.app.bsky.feed.defs.SkeletonFeedPost
7
8
import io.ktor.serialization.kotlinx.json.*
8
9
import io.ktor.server.application.*
9
10
import io.ktor.server.engine.*
···
17
18
18
19
val DESIRED_LABELS: List<String> = listOf("porn", "sexual", "nudity", "sexual-figurative")
19
20
20
-
class DarkFeedApi(
21
+
class FeedServer(
21
22
private val hostname: String,
22
23
private val bskyApi: BskyApi,
23
24
private val port: Int = 8080,
-10
app/src/main/kotlin/lexicon/app/bsky/feed/FeedSkeleton.kt
-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
-
)
+1
-1
app/src/main/kotlin/lexicon/app/bsky/feed/Generator.kt
darkfeed/src/main/kotlin/api/lexicon/app/bsky/feed/Generator.kt
+1
-1
app/src/main/kotlin/lexicon/app/bsky/feed/Generator.kt
darkfeed/src/main/kotlin/api/lexicon/app/bsky/feed/Generator.kt
+3
-3
app/src/main/kotlin/lexicon/app/bsky/feed/Like.kt
darkfeed/src/main/kotlin/api/lexicon/app/bsky/feed/Like.kt
+3
-3
app/src/main/kotlin/lexicon/app/bsky/feed/Like.kt
darkfeed/src/main/kotlin/api/lexicon/app/bsky/feed/Like.kt
···
1
-
package gay.averyrivers.lexicon.app.bsky.feed
1
+
package api.lexicon.app.bsky.feed
2
2
3
-
import gay.averyrivers.lexicon.com.atproto.repo.StrongRef
3
+
import api.lexicon.com.atproto.repo.StrongRef
4
4
import kotlinx.serialization.Serializable
5
5
6
6
@Serializable
···
14
14
val uri: String,
15
15
val cid: String,
16
16
val value: Like,
17
-
)
17
+
)
+2
-2
app/src/main/kotlin/lexicon/app/bsky/feed/Post.kt
darkfeed/src/main/kotlin/api/lexicon/app/bsky/feed/Post.kt
+2
-2
app/src/main/kotlin/lexicon/app/bsky/feed/Post.kt
darkfeed/src/main/kotlin/api/lexicon/app/bsky/feed/Post.kt
-11
app/src/main/kotlin/lexicon/app/bsky/feed/defs/PostView.kt
-11
app/src/main/kotlin/lexicon/app/bsky/feed/defs/PostView.kt
+2
-2
app/src/main/kotlin/lexicon/app/bsky/feed/defs/SkeletonFeedPost.kt
darkfeed/src/main/kotlin/api/lexicon/app/bsky/feed/defs/SkeletonFeedPost.kt
+2
-2
app/src/main/kotlin/lexicon/app/bsky/feed/defs/SkeletonFeedPost.kt
darkfeed/src/main/kotlin/api/lexicon/app/bsky/feed/defs/SkeletonFeedPost.kt
+1
-1
app/src/main/kotlin/lexicon/com/atproto/label/defs/Label.kt
darkfeed/src/main/kotlin/api/lexicon/com/atproto/label/defs/Label.kt
+1
-1
app/src/main/kotlin/lexicon/com/atproto/label/defs/Label.kt
darkfeed/src/main/kotlin/api/lexicon/com/atproto/label/defs/Label.kt
+2
-2
app/src/main/kotlin/lexicon/com/atproto/repo/StrongRef.kt
darkfeed/src/main/kotlin/api/lexicon/com/atproto/repo/StrongRef.kt
+2
-2
app/src/main/kotlin/lexicon/com/atproto/repo/StrongRef.kt
darkfeed/src/main/kotlin/api/lexicon/com/atproto/repo/StrongRef.kt
app/src/main/resources/logback.xml
darkfeed/src/main/resources/logback.xml
app/src/main/resources/logback.xml
darkfeed/src/main/resources/logback.xml
+108
darkfeed/src/main/kotlin/Main.kt
+108
darkfeed/src/main/kotlin/Main.kt
···
1
+
import api.BskyApi
2
+
import api.lexicon.app.bsky.feed.Generator
3
+
import io.ktor.http.*
4
+
import kotlinx.coroutines.launch
5
+
import kotlinx.coroutines.runBlocking
6
+
import server.FeedServer
7
+
import kotlin.system.exitProcess
8
+
9
+
/**
10
+
*
11
+
*/
12
+
data class AppContext(
13
+
/** PDS of the feed owner's account. */
14
+
val ownerPds: String,
15
+
/** DID of the feed owner's account. */
16
+
val ownerDid: String,
17
+
/** Password for the feed owner's account. */
18
+
val ownerPassword: String,
19
+
/** Hostname of the feed generator server. */
20
+
val hostname: String,
21
+
/** Record key for the feed generator record. */
22
+
val recordKey: String = "darkfeed",
23
+
/** Display name for the feed. */
24
+
val feedDisplayName: String = "DarkFeed",
25
+
/** Description for the feed. */
26
+
val description: String = "hi :3",
27
+
)
28
+
29
+
/**
30
+
* Print a message and exit the application.
31
+
*
32
+
* @param message Message to print.
33
+
* @param code Status code to exit with.
34
+
*/
35
+
fun printMessageAndExit(message: String, code: Int = 1): Nothing {
36
+
println(message)
37
+
exitProcess(code)
38
+
}
39
+
40
+
/**
41
+
* Verify the current feed generator record, creating or updating it if necessary.
42
+
*
43
+
* @param api Bluesky API instance. Requires login.
44
+
* @param ctx Application context.
45
+
*/
46
+
suspend fun verifyAndUpdateFeedGeneratorRecord(api: BskyApi, ctx: AppContext) {
47
+
// Get the current record stored in the repo.
48
+
var feedGeneratorRecord = api.getFeedGeneratorRecord(ctx.ownerDid, ctx.recordKey)
49
+
50
+
// TODO: Check all fields of the record against the context.
51
+
// If the current record exists and has the correct DID, nothing needs to be done.
52
+
if (feedGeneratorRecord?.did?.contains(ctx.hostname) == true) return
53
+
54
+
// Update the current record if one exists, or create a new one if it doesn't.
55
+
feedGeneratorRecord = feedGeneratorRecord
56
+
?.copy(did = "did:web:${ctx.hostname}")
57
+
?: Generator(
58
+
did = "did:web:${ctx.hostname}",
59
+
displayName = ctx.feedDisplayName,
60
+
description = ctx.description,
61
+
// TODO: Use the real time here.
62
+
createdAt = "2024-11-04T15:58:05.074Z"
63
+
)
64
+
65
+
// Store the new/updated record in the repo.
66
+
api.putFeedGeneratorRecord(ctx.ownerDid, ctx.recordKey, feedGeneratorRecord)
67
+
}
68
+
69
+
fun main() = runBlocking {
70
+
// Create app context from environment variables.
71
+
val ctx = AppContext(
72
+
ownerPds = System.getenv("FEED_ACCOUNT_PDS")
73
+
?: "bsky.social",
74
+
ownerDid = System.getenv("FEED_ACCOUNT_DID")
75
+
?: printMessageAndExit("error: variable FEED_ACCOUNT_DID not set"),
76
+
ownerPassword = System.getenv("FEED_ACCOUNT_PASSWORD")
77
+
?: printMessageAndExit("error: variable FEED_ACCOUNT_PASSWORD not set"),
78
+
hostname = System.getenv("HOSTNAME")
79
+
?: printMessageAndExit("error: variable HOSTNAME not set"),
80
+
)
81
+
82
+
// Create API instance.
83
+
val bskyApi = BskyApi(buildUrl {
84
+
protocol = URLProtocol.HTTPS
85
+
host = ctx.ownerPds
86
+
})
87
+
88
+
// Verify and update the feed generator record.
89
+
launch {
90
+
bskyApi.login(ctx.ownerDid, ctx.ownerPassword)
91
+
92
+
try {
93
+
verifyAndUpdateFeedGeneratorRecord(bskyApi, ctx)
94
+
println("main: feed generator record verified")
95
+
} catch (error: Exception) {
96
+
println("main: failed to verify and update feed generator record: ${error.message}")
97
+
}
98
+
}.join()
99
+
100
+
println("main: starting feed generator server")
101
+
102
+
// Start the feed server.
103
+
FeedServer(
104
+
hostname = ctx.hostname,
105
+
bskyApi = bskyApi,
106
+
port = 8080,
107
+
).serve()
108
+
}
+10
darkfeed/src/main/kotlin/api/lexicon/app/bsky/feed/FeedSkeleton.kt
+10
darkfeed/src/main/kotlin/api/lexicon/app/bsky/feed/FeedSkeleton.kt
+11
darkfeed/src/main/kotlin/api/lexicon/app/bsky/feed/defs/PostView.kt
+11
darkfeed/src/main/kotlin/api/lexicon/app/bsky/feed/defs/PostView.kt
+3
-8
fly.toml
+3
-8
fly.toml
···
1
-
# fly.toml app configuration file generated for darkfeed on 2024-11-05T20:07:05-05:00
2
-
#
3
-
# See https://fly.io/docs/reference/configuration/ for information about how to use this file.
4
-
#
5
-
6
1
app = 'darkfeed'
7
2
primary_region = 'iad'
8
3
···
23
18
memory = '512mb'
24
19
25
20
[env]
26
-
DF_OWNER_PDS = 'bsky.social'
27
-
DF_OWNER_DID = 'did:plc:zhxv5pxpmojhnvaqy4mwailv'
28
-
DF_HOSTNAME = 'darkfeed.fly.dev'
21
+
FEED_ACCOUNT_PDS = 'bsky.social'
22
+
FEED_ACCOUNT_DID = 'did:plc:zhxv5pxpmojhnvaqy4mwailv'
23
+
HOSTNAME = 'darkfeed.fly.dev'