implementation group: 'com.google.code.findbugs', name: 'jsr305', version: '3.0.2'
testImplementation group: "junit", name: "junit", version: "4.13.2"
- testImplementation group: "org.hamcrest", name: "hamcrest-library", version: "1.3"
+ testImplementation group: "org.hamcrest", name: "hamcrest", version: "2.2"
testImplementation group: "org.mockito", name: "mockito-core", version: "5.11.0"
testImplementation group: "com.spotify", name: "hamcrest-jackson", version: "1.3.2"
}
import com.fasterxml.jackson.databind.JsonNode
import net.pterodactylus.rhynodge.Filter
import net.pterodactylus.rhynodge.State
-import net.pterodactylus.rhynodge.states.AbstractState
import net.pterodactylus.rhynodge.states.JsonState
import java.time.Instant
private fun getPromotionalOfferDate(gameJson: JsonNode, date: String) = listOf("promotionalOffers", "upcomingPromotionalOffers")
.map { "/promotions/$it/0/promotionalOffers/0/$date" }
- .map { gameJson.at(it) }
- .filter { !it.isMissingNode }
- .map { it.asText() }
+ .map(gameJson::at)
+ .filterNot(JsonNode::isMissingNode)
+ .map(JsonNode::asText)
.map(Instant::parse)
.first()
}
-
-data class FreeGame(
- val title: String,
- val imageUrl: String,
- val startDate: Instant,
- val endDate: Instant
-)
-
-class FreeGamesState(val games: Set<FreeGame>) : AbstractState(true) {
-
- override fun plainText(): String {
- TODO("Not yet implemented")
- }
-
-}
--- /dev/null
+package net.pterodactylus.rhynodge.filters.webpages.epicgames
+
+import net.pterodactylus.rhynodge.Merger
+import net.pterodactylus.rhynodge.State
+
+class EpicGamesMerger : Merger {
+
+ override fun mergeStates(previousState: State, currentState: State): State {
+ previousState as? FreeGamesState ?: throw IllegalArgumentException("previousState is not a FreeGamesState")
+ currentState as? FreeGamesState ?: throw IllegalArgumentException("currentState is not a FreeGamesState")
+
+ val oldGames = previousState.games
+ val newGames = currentState.games
+
+ return FreeGamesState(newGames, (newGames - oldGames).isNotEmpty())
+ }
+
+}
--- /dev/null
+package net.pterodactylus.rhynodge.filters.webpages.epicgames
+
+import java.time.Instant
+
+data class FreeGame(
+ val title: String,
+ val imageUrl: String,
+ val startDate: Instant,
+ val endDate: Instant
+)
--- /dev/null
+package net.pterodactylus.rhynodge.filters.webpages.epicgames
+
+import kotlinx.html.body
+import kotlinx.html.div
+import kotlinx.html.dom.createHTMLDocument
+import kotlinx.html.dom.serialize
+import kotlinx.html.head
+import kotlinx.html.html
+import kotlinx.html.img
+import kotlinx.html.style
+import kotlinx.html.unsafe
+import net.pterodactylus.rhynodge.states.AbstractState
+import java.time.ZoneId
+import java.util.Comparator.comparing
+
+class FreeGamesState(val games: Set<FreeGame>, private val triggered: Boolean = false, private val timezone: ZoneId = ZoneId.systemDefault()) : AbstractState(true) {
+
+ override fun plainText() = games
+ .sortedWith(comparing(FreeGame::startDate).thenBy(FreeGame::title))
+ .joinToString("\n") { game ->
+ "${game.title}: ${"%tF %<tT".format(game.startDate.atZone(timezone))} - ${"%tF %<tT".format(game.endDate.atZone(timezone))} (${game.imageUrl})"
+ }
+
+ override fun htmlText() = createHTMLDocument().html {
+ head {
+ style("text/css") {
+ unsafe {
+ raw(
+ """
+ .game { display: inline-grid; width: 200px; height: 350px; grid-template-rows: 0fr 1fr 0fr 0fr; font-family: 'Recursive Sans Linear Static', Roboto, serif; color: white; text-shadow: 2px 2px black; margin: 0 1ex 1ex 0; }
+ .game .game-image { grid-area: 1/1/5/2; }
+ .game .game-image img { object-fit: cover; width: 100%; height: 100%; }
+ .game .game-title { grid-area: 1/1/2/2; padding: 1ex; font-size: 150%; font-weight: bold; }
+ .game .game-start { grid-area: 3/1/4/2; padding: 1ex 1ex 0ex 1ex; }
+ .game .game-end { grid-area: 4/1/5/2; padding: 0ex 1ex 1ex 1ex; }
+ """
+ )
+ }
+ }
+ }
+ body {
+ div("games") {
+ games
+ .sortedWith(comparing(FreeGame::startDate).thenBy(FreeGame::title))
+ .forEach { game ->
+ div("game") {
+ div("game-image") {
+ img(src = game.imageUrl)
+ }
+ div("game-title") {
+ +game.title
+ }
+ div("game-start") {
+ +"%tF %<tT".format(game.startDate.atZone(timezone))
+ }
+ div("game-end") {
+ +"%tF %<tT".format(game.endDate.atZone(timezone))
+ }
+ }
+ }
+ }
+ }
+ }.serialize(prettyPrint = true)
+
+ override fun triggered() = triggered
+
+}
--- /dev/null
+package net.pterodactylus.rhynodge.watchers
+
+import net.pterodactylus.rhynodge.filters.JsonFilter
+import net.pterodactylus.rhynodge.filters.webpages.epicgames.EpicGamesFilter
+import net.pterodactylus.rhynodge.filters.webpages.epicgames.EpicGamesMerger
+import net.pterodactylus.rhynodge.queries.HttpQuery
+
+class EpicGamesWatcher : DefaultWatcher(query, filters, merger)
+
+private val query = HttpQuery("https://store-site-backend-static-ipv4.ak.epicgames.com/freeGamesPromotions?locale=en-US&country=DE&allowCountries=DE")
+private val filters = listOf(JsonFilter(), EpicGamesFilter())
+private val merger = EpicGamesMerger()
--- /dev/null
+package net.pterodactylus.rhynodge.filters.webpages.epicgames
+
+import net.pterodactylus.rhynodge.Merger
+import net.pterodactylus.rhynodge.states.StateManagerTest.TestState
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.contains
+import org.hamcrest.Matchers.empty
+import org.hamcrest.Matchers.equalTo
+import org.hamcrest.Matchers.instanceOf
+import org.junit.Assert.assertThrows
+import org.junit.Test
+import java.time.Instant
+
+class EpicGamesMergerTest {
+
+ @Test
+ fun `merger implements Merger interface`() {
+ assertThat(merger, instanceOf(Merger::class.java))
+ }
+
+ @Test
+ fun `merger throws exception if previous state is not a free games state`() {
+ assertThrows(IllegalArgumentException::class.java) { merger.mergeStates(TestState(), emptyState) }
+ }
+
+ @Test
+ fun `merger throws exception if current state is not a free games state`() {
+ assertThrows(IllegalArgumentException::class.java) { merger.mergeStates(emptyState, TestState()) }
+ }
+
+ @Test
+ fun `merger given two empty free game states returns an empty state`() {
+ val mergedState = merger.mergeStates(emptyState, emptyState) as FreeGamesState
+ assertThat(mergedState.games, empty())
+ }
+
+ @Test
+ fun `merging two empty states results in not-triggered state`() {
+ val mergedState = merger.mergeStates(emptyState, emptyState) as FreeGamesState
+ assertThat(mergedState.triggered(), equalTo(false))
+ }
+
+ @Test
+ fun `merging state without games into state with games returns state without games`() {
+ val mergedState = merger.mergeStates(stateWithOneGame, emptyState) as FreeGamesState
+ assertThat(mergedState.games, empty())
+ }
+
+ @Test
+ fun `merging state without games into state with games returns not-triggered state`() {
+ val mergedState = merger.mergeStates(stateWithOneGame, emptyState) as FreeGamesState
+ assertThat(mergedState.triggered(), equalTo(false))
+ }
+
+ @Test
+ fun `merging state with games into state without games returns state with games`() {
+ val mergedState = merger.mergeStates(emptyState, stateWithOneGame) as FreeGamesState
+ assertThat(mergedState.games, contains(game1))
+ }
+
+ @Test
+ fun `merging state with games into state without games returns a triggered state`() {
+ val mergedState = merger.mergeStates(emptyState, stateWithOneGame) as FreeGamesState
+ assertThat(mergedState.triggered(), equalTo(true))
+ }
+
+ @Test
+ fun `merge state with more games into state with less games returns a triggered state`() {
+ val mergedState = merger.mergeStates(stateWithOneGame, stateWithTwoGames) as FreeGamesState
+ assertThat(mergedState.triggered(), equalTo(true))
+ }
+
+ @Test
+ fun `merge state with less games into state with more games does not return a triggered state`() {
+ val mergedState = merger.mergeStates(stateWithTwoGames, stateWithOneGame) as FreeGamesState
+ assertThat(mergedState.triggered(), equalTo(false))
+ }
+
+ private val merger = EpicGamesMerger()
+
+}
+
+private val emptyState = FreeGamesState(emptySet())
+private val game1 = FreeGame("1", "i", Instant.now(), Instant.now())
+private val game2 = FreeGame("2", "i", Instant.now(), Instant.now())
+private val stateWithOneGame = FreeGamesState(setOf(game1))
+private val stateWithTwoGames = FreeGamesState(setOf(game1, game2))
--- /dev/null
+package net.pterodactylus.rhynodge.filters.webpages.epicgames
+
+import net.pterodactylus.rhynodge.Reaction
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.contains
+import org.hamcrest.Matchers.equalTo
+import org.jsoup.Jsoup
+import org.junit.Test
+import java.time.Instant
+import java.time.ZoneOffset
+
+class FreeGamesStateTest {
+
+ @Test
+ fun `can create free games state`() {
+ FreeGamesState(emptySet())
+ }
+
+ @Test
+ fun `state lists all games in text output`() {
+ val output = state.output(Reaction("", null, null, null)).text("text/plain")
+ assertThat(
+ output, equalTo(
+ listOf(
+ "Good Game: 1970-01-01 00:16:40 - 1970-01-01 00:33:20 (https://good.game/image.jpg)",
+ "Best Game: 1970-01-01 00:33:20 - 1970-01-01 00:50:00 (https://best.game/image.webp)",
+ "Better Game: 1970-01-01 00:50:00 - 1970-01-01 01:06:40 (https://better.game/image.png)",
+ ).joinToString("\n")
+ )
+ )
+ }
+
+ @Test
+ fun `state lists all games in HTML output`() {
+ val output = state.output(Reaction("", null, null, null)).text("text/html")
+ val parsedOutput = Jsoup.parse(output)
+ assertThat(
+ parsedOutput.select(".game").map {
+ listOf(
+ it.select(".game-title").text(),
+ it.select(".game-image img").attr("src"),
+ it.select(".game-start").text(),
+ it.select(".game-end").text()
+ ).joinToString(", ")
+ }, contains(
+ "Good Game, https://good.game/image.jpg, 1970-01-01 00:16:40, 1970-01-01 00:33:20",
+ "Best Game, https://best.game/image.webp, 1970-01-01 00:33:20, 1970-01-01 00:50:00",
+ "Better Game, https://better.game/image.png, 1970-01-01 00:50:00, 1970-01-01 01:06:40",
+ )
+ )
+ }
+
+}
+
+private val freeGames = setOf(
+ FreeGame("Good Game", "https://good.game/image.jpg", Instant.ofEpochSecond(1000), Instant.ofEpochSecond(2000)),
+ FreeGame("Better Game", "https://better.game/image.png", Instant.ofEpochSecond(3000), Instant.ofEpochSecond(4000)),
+ FreeGame("Best Game", "https://best.game/image.webp", Instant.ofEpochSecond(2000), Instant.ofEpochSecond(3000)),
+)
+private val state = FreeGamesState(freeGames, timezone = ZoneOffset.UTC)