An overengineered shortcut to Slay the Spire’s daily challenge page
Slay the Spire is a great roguelike deckbuilder with daily challenges.
A special bot creates daily posts on Reddit, which list the features of today’s challenge.
When I want to check out the new challenge and don’t have access to the game, I have to open the game’s subreddit, find the necessary thread and open it.
It requires two clicks! Unacceptable, right?
This single-file Kotlin application periodically checks for new daily posts and, when requested, redirects to the newest one. That’s all. At least I practiced Kotlin a bit.
package com.httpain.spire
import org.eclipse.jetty.server.Request
import org.eclipse.jetty.server.Server
import org.eclipse.jetty.server.ServerConnector
import org.eclipse.jetty.server.handler.AbstractHandler
import org.eclipse.jetty.util.log.Log
import org.jsoup.Jsoup
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse
fun main(args: Array<String>) {
val server = Server()
val http = ServerConnector(server)
http.host = "localhost"
http.port = 8080
// app does one redirect and nothing more, not worth keeping the connection
http.idleTimeout = 0
server.addConnector(http)
server.handler = RedirectingHandler { LinkScraper.cachedLink }
server.start()
server.join()
}
class RedirectingHandler(val linkProvider: () -> String) : AbstractHandler() {
override fun handle(
target: String?, baseRequest: Request?,
request: HttpServletRequest?, response: HttpServletResponse?
) {
// re-evaluate linkProvider each time to get fresh link
response!!.sendRedirect(linkProvider())
}
}
object LinkScraper {
private const val latestPostsPage = "https://www.reddit.com/user/StSDailyBot"
private const val timeoutMillis = 30 * 1000
private const val periodMinutes = 15L
private val log = Log.getLogger(LinkScraper.javaClass)
private val executor = Executors.newSingleThreadScheduledExecutor()
@Volatile
var cachedLink: String
private set
init {
cachedLink = getLink()
executor.scheduleWithFixedDelay(
{
try {
cachedLink = getLink()
log.info("Link to newest post: $cachedLink")
} catch (e: Exception) {
log.warn(e)
}
},
periodMinutes, periodMinutes, TimeUnit.MINUTES
)
}
private fun getLink(): String {
val document = Jsoup.connect(latestPostsPage).timeout(timeoutMillis).get()
// class names are generated/obfuscated, attribute presence is more reliable
return document.select("""a[data-click-id="body"]""")
// absUrl converts links to absolute
.map { element -> element.absUrl("href") }
.first { href -> href.contains("daily_run") }
}
}
And here’s a sample build.gradle with fat jar support, which was enough to deploy it wherever I wanted:
plugins {
id 'org.jetbrains.kotlin.jvm' version '1.2.70'
}
group 'com.httpain'
version '1.0-SNAPSHOT'
repositories {
mavenCentral()
}
jar {
manifest {
attributes "Main-Class": 'com.httpain.spire.ApplicationKt'
}
from {
configurations.compile.collect { it.isDirectory() ? it : zipTree(it) }
}
}
dependencies {
compile 'org.jetbrains.kotlin:kotlin-stdlib-jdk8'
compile 'org.eclipse.jetty:jetty-server:9.4.12.v20180830' // to handle HTTP requests
compile 'org.jsoup:jsoup:1.11.3' // to parse Reddit HTML
}
compileKotlin {
kotlinOptions.jvmTarget = "1.8"
}
compileTestKotlin {
kotlinOptions.jvmTarget = "1.8"
}