···11+import api.BskyApi
22+import api.lexicon.app.bsky.feed.Generator
33+import io.ktor.http.*
44+import kotlinx.coroutines.launch
55+import kotlinx.coroutines.runBlocking
66+import server.FeedServer
77+import kotlin.system.exitProcess
88+99+/**
1010+ *
1111+ */
1212+data class AppContext(
1313+ /** PDS of the feed owner's account. */
1414+ val ownerPds: String,
1515+ /** DID of the feed owner's account. */
1616+ val ownerDid: String,
1717+ /** Password for the feed owner's account. */
1818+ val ownerPassword: String,
1919+ /** Hostname of the feed generator server. */
2020+ val hostname: String,
2121+ /** Record key for the feed generator record. */
2222+ val recordKey: String = "darkfeed",
2323+ /** Display name for the feed. */
2424+ val feedDisplayName: String = "DarkFeed",
2525+ /** Description for the feed. */
2626+ val description: String = "hi :3",
2727+)
2828+2929+/**
3030+ * Print a message and exit the application.
3131+ *
3232+ * @param message Message to print.
3333+ * @param code Status code to exit with.
3434+ */
3535+fun printMessageAndExit(message: String, code: Int = 1): Nothing {
3636+ println(message)
3737+ exitProcess(code)
3838+}
3939+4040+/**
4141+ * Verify the current feed generator record, creating or updating it if necessary.
4242+ *
4343+ * @param api Bluesky API instance. Requires login.
4444+ * @param ctx Application context.
4545+ */
4646+suspend fun verifyAndUpdateFeedGeneratorRecord(api: BskyApi, ctx: AppContext) {
4747+ // Get the current record stored in the repo.
4848+ var feedGeneratorRecord = api.getFeedGeneratorRecord(ctx.ownerDid, ctx.recordKey)
4949+5050+ // TODO: Check all fields of the record against the context.
5151+ // If the current record exists and has the correct DID, nothing needs to be done.
5252+ if (feedGeneratorRecord?.did?.contains(ctx.hostname) == true) return
5353+5454+ // Update the current record if one exists, or create a new one if it doesn't.
5555+ feedGeneratorRecord = feedGeneratorRecord
5656+ ?.copy(did = "did:web:${ctx.hostname}")
5757+ ?: Generator(
5858+ did = "did:web:${ctx.hostname}",
5959+ displayName = ctx.feedDisplayName,
6060+ description = ctx.description,
6161+ // TODO: Use the real time here.
6262+ createdAt = "2024-11-04T15:58:05.074Z"
6363+ )
6464+6565+ // Store the new/updated record in the repo.
6666+ api.putFeedGeneratorRecord(ctx.ownerDid, ctx.recordKey, feedGeneratorRecord)
6767+}
6868+6969+fun main() = runBlocking {
7070+ // Create app context from environment variables.
7171+ val ctx = AppContext(
7272+ ownerPds = System.getenv("FEED_ACCOUNT_PDS")
7373+ ?: "bsky.social",
7474+ ownerDid = System.getenv("FEED_ACCOUNT_DID")
7575+ ?: printMessageAndExit("error: variable FEED_ACCOUNT_DID not set"),
7676+ ownerPassword = System.getenv("FEED_ACCOUNT_PASSWORD")
7777+ ?: printMessageAndExit("error: variable FEED_ACCOUNT_PASSWORD not set"),
7878+ hostname = System.getenv("HOSTNAME")
7979+ ?: printMessageAndExit("error: variable HOSTNAME not set"),
8080+ )
8181+8282+ // Create API instance.
8383+ val bskyApi = BskyApi(buildUrl {
8484+ protocol = URLProtocol.HTTPS
8585+ host = ctx.ownerPds
8686+ })
8787+8888+ // Verify and update the feed generator record.
8989+ launch {
9090+ bskyApi.login(ctx.ownerDid, ctx.ownerPassword)
9191+9292+ try {
9393+ verifyAndUpdateFeedGeneratorRecord(bskyApi, ctx)
9494+ println("main: feed generator record verified")
9595+ } catch (error: Exception) {
9696+ println("main: failed to verify and update feed generator record: ${error.message}")
9797+ }
9898+ }.join()
9999+100100+ println("main: starting feed generator server")
101101+102102+ // Start the feed server.
103103+ FeedServer(
104104+ hostname = ctx.hostname,
105105+ bskyApi = bskyApi,
106106+ port = 8080,
107107+ ).serve()
108108+}