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"
}