diff --git a/KamerZoeken-1.0.0.jar b/KamerZoeken-1.0.0.jar deleted file mode 100644 index e70d872..0000000 Binary files a/KamerZoeken-1.0.0.jar and /dev/null differ diff --git a/README.md b/README.md index d4c0d01..1babf6e 100644 --- a/README.md +++ b/README.md @@ -7,10 +7,12 @@ If you want a command that just downloads, compiles and runs everything, run thi ```sh git clone https://github.com/KoenDR06/KamerZoeken cd KamerZoeken + ``` -Then, set your preferences in [`config.toml`](config.toml) and set your credentials in `.env` (you can find what secrets are needed in [`sample.env`](sample.env)). After you have done this, you are ready to run the script using: +Then, set your preferences in [config.toml](config.toml). After you have done this, run the following script: ```sh -java -jar KamerZoeken-1.0.0.jar -``` +./gradlew build +java -jar build/libs/KamerZoeken-1.0.0.jar +``` \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index eba1be9..84c66e7 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -15,7 +15,6 @@ dependencies { implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.1") implementation("de.thelooter:toml4j:0.8.1") implementation("io.github.cdimascio:dotenv-kotlin:6.5.1") - implementation(files("../Utils-latest.jar")) } tasks.test { diff --git a/config.toml b/config.toml index ef45bb8..e50df5d 100644 --- a/config.toml +++ b/config.toml @@ -1,6 +1,6 @@ [general] unitType = "Kamer" # Kamer of Woning -cities = ["ZEIST", "UTRECHT"] # Steden om in te zoeken +allowZeist = true # Of je ook in Zeist wil zoeken smoking = -1 # 0: geen voorkeur, -1: ik wil dat er niet gerookt mag worden, 1: Ik wil dat er gerookt mag worden pets = 0 # 0: geen voorkeur, -1: ik wil dat er geen huisdieren mogen, 1: Ik wil dat huisdieren mogen diff --git a/sample.env b/sample.env index 4793ec5..b2dd23a 100644 --- a/sample.env +++ b/sample.env @@ -1,2 +1 @@ -EMAIL=test@example.com -PASSWORD="123456789" +AUTH= \ No newline at end of file diff --git a/src/main/kotlin/Main.kt b/src/main/kotlin/Main.kt index 0ff2d82..41d4d9d 100644 --- a/src/main/kotlin/Main.kt +++ b/src/main/kotlin/Main.kt @@ -1,7 +1,6 @@ package me.koendev import io.github.cdimascio.dotenv.Dotenv -import me.koendev.utils.println import java.io.File import java.time.LocalDate import java.time.format.DateTimeFormatter @@ -15,111 +14,83 @@ data class ReactableOffer( val floor: Floor ) -fun main(args: Array) { - val headless = args.size == 1 && args[0] == "headless" - - if (!headless) print("Authenticating") - val sessionToken = auth() - - if (!headless) print("\rGetting rooms ") +fun main() { val rooms = getRooms().filter { room -> room.unitType == config.general.unitType } if (rooms.isEmpty()) { - System.err.println("No suitable offers were found, quitting.") + println("No suitable offers were found, quitting.") return } - val offers = getOffers(rooms.map { it.wocasId }).offers.filter { offer -> - offer.adres[0].plaats in config.general.cities + offer.adres[0].plaats in listOf( + "UTRECHT", + ) + if (config.general.allowZeist) "ZEIST" else "" } - var index = 0 val coupled = rooms.mapNotNull { room -> val offer: Offer? = offers.find { room.wocasId.toInt() == it.eenheidNummer.toInt() } - if (!headless) print("\rFiltering per room: ${index++} / ${rooms.size}") - if (offer == null) null else ReactableOffer(room, offer, getFloorInfo(room, sessionToken)) + if (offer == null) null else ReactableOffer(room, offer, getFloorInfo(room)) }.filter { val gender = it.floor.floorInfo.genderPreference - val smoking = it.floor.floorInfo.smokingAllowed ?: true - val pets = it.floor.floorInfo.petsAllowed ?: false - val date = it.room.expireBy.take(10) val date1 = LocalDate.now() val date2 = LocalDate.parse(date, DateTimeFormatter.ofPattern("yyyy-MM-dd")) - - val days = ChronoUnit.DAYS.between(date1, date2) + val daysLeft = ChronoUnit.DAYS.between(date1, date2) ((gender == "female" && config.gender.female) || (gender == "male" && config.gender.male) || (gender == "none" && config.gender.none)) && - ((config.general.smoking == -1 && !smoking) || (config.general.smoking == 1 && smoking) || config.general.smoking == 0) && + ((config.general.smoking == -1 && !(it.floor.floorInfo.smokingAllowed ?: true)) || (config.general.smoking == 1 && it.floor.floorInfo.smokingAllowed ?: true) || config.general.smoking == 0) && - ((config.general.pets == -1 && !pets) || (config.general.pets == 1 && pets) || config.general.pets == 0) && + ((config.general.pets == -1 && !it.floor.floorInfo.petsAllowed) || (config.general.pets == 1 && it.floor.floorInfo.petsAllowed) || config.general.pets == 0) && + + daysLeft == 0L + } + + val fileName = "offers.md" + val out = File(fileName) + if (!out.exists()) out.createNewFile() + + val str = StringBuilder() + + coupled.forEach { + val address = it.offer.adres[0] + + str.append("## [${address.straatnaam} ${address.nummer}, ${address.plaats.lowercase().capitalize()}](https://sshxl.nl/nl/aanbod/${it.room.flowId}-${address.straatnaam.lowercase().replace(" ", "-")})\n") + + str.append("\n| Categorie | Waarde |\n") + str.append("|-------------|--------------------|\n") + + str.append("| Huisgenoten | ${(it.room.numberOfRooms-1).toString().padEnd(18, ' ')} |\n") + + val genderString = when (it.floor.floorInfo.genderPreference) { + "none" -> "Geen voorkeur" + "male" -> "Man" + "female" -> "Vrouw" + else -> it.floor.floorInfo.genderPreference + } + str.append("| Geslacht | ${genderString.padEnd(18, ' ')} |\n") + + + str.append("| Roken | ${(if (it.floor.floorInfo.smokingAllowed ?: true) "✅ Mag" else "❌ Mag niet").padEnd(17, ' ')} |\n") + str.append("| Huisdieren | ${(if (it.floor.floorInfo.petsAllowed) "✅ Mogen" else "❌ Mogen niet").padEnd(17, ' ')} |\n") + str.append("| Reacties | ${it.floor.potentialPosition} van de ${it.floor.applicantCount.toString().padStart(3, ' ')}. |\n") - days == 0L - }.sortedBy { - it.floor.potentialPosition - }.sortedBy { val date = it.room.expireBy.take(10) val date1 = LocalDate.now() val date2 = LocalDate.parse(date, DateTimeFormatter.ofPattern("yyyy-MM-dd")) + val daysLeft = ChronoUnit.DAYS.between(date1, date2) + str.append("| Tijd over | $daysLeft dagen over. |\n") - ChronoUnit.DAYS.between(date1, date2) - } - - if (!headless) { - val fileName = "offers.md" - val out = File(fileName) - if (!out.exists()) out.createNewFile() - - val str = StringBuilder() - - coupled.forEach { - val address = it.offer.adres[0] - - str.append("## [${address.straatnaam} ${address.nummer}, ${address.plaats.lowercase().replaceFirstChar { if (it.isLowerCase()) it - 32 else it }}](https://sshxl.nl/nl/aanbod/${it.room.flowId}-${address.straatnaam.lowercase().replace(" ", "-")})\n") - - str.append("\n| Categorie | Waarde |\n") - str.append("|-------------|--------------------|\n") - - str.append("| Huisgenoten | ${(it.room.numberOfRooms-1).toString().padEnd(18, ' ')} |\n") - - val genderString = when (it.floor.floorInfo.genderPreference) { - "none" -> "Geen voorkeur" - "male" -> "Man" - "female" -> "Vrouw" - else -> it.floor.floorInfo.genderPreference - } - str.append("| Geslacht | ${genderString.padEnd(18, ' ')} |\n") - - str.append("| Roken | ${(if (it.floor.floorInfo.smokingAllowed ?: true) "✅ Mag" else "❌ Mag niet").padEnd(17, ' ')} |\n") - str.append("| Huisdieren | ${(if (it.floor.floorInfo.petsAllowed ?: false) "✅ Mogen" else "❌ Mogen niet").padEnd(17, ' ')} |\n") - val positionString = "${it.floor.potentialPosition} / ${it.floor.applicantCount}." - str.append("| Reacties | ${positionString.padEnd(18, ' ')} |\n") - - val date = it.room.expireBy.take(10) - val date1 = LocalDate.now() - val date2 = LocalDate.parse(date, DateTimeFormatter.ofPattern("yyyy-MM-dd")) - val daysLeft = ChronoUnit.DAYS.between(date1, date2) - str.append("| Tijd over | $daysLeft dagen over. |\n") - - str.append("\n") - - str.append("### Message: \n\n${it.floor.floorInfo.description ?: "Deze pannekoeken hebben geen bericht achtergelaten"}\n") - - str.append("\n\n") - } - out.writeText(str.toString()) - println("\r${coupled.size} offers found, wrote to $fileName") - } else { - coupled.filter { - it.floor.potentialPosition <= 20 - }.forEach { - postEndpoint("reactions/${it.room.flowId}", mapOf(), sessionToken) - } + str.append("\n") + str.append("### Message: \n\n${it.floor.floorInfo.description ?: "Deze pannekoeken hebben geen bericht achtergelaten"}\n") + + str.append("\n\n") } + out.writeText(str.toString()) + println("${coupled.size} offers found, wrote to $fileName") } diff --git a/src/main/kotlin/TOML.kt b/src/main/kotlin/TOML.kt index 9ed4d57..83b1195 100644 --- a/src/main/kotlin/TOML.kt +++ b/src/main/kotlin/TOML.kt @@ -5,7 +5,7 @@ import java.io.File data class GeneralConfig( val unitType: String, - val cities: List, + val allowZeist: Boolean, val smoking: Int, val pets: Int ) @@ -21,4 +21,4 @@ data class Config( val gender: GenderConfig, ) -val config: Config = Toml().read(File("config.toml")).to(Config::class.java) +val config: Config = Toml().read(File("config.toml")).to(Config::class.java) \ No newline at end of file diff --git a/src/main/kotlin/authenticate.kt b/src/main/kotlin/authenticate.kt deleted file mode 100644 index 8598c69..0000000 --- a/src/main/kotlin/authenticate.kt +++ /dev/null @@ -1,51 +0,0 @@ -package me.koendev - -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonIgnoreUnknownKeys -import me.koendev.utils.println -import java.net.URI -import java.net.http.HttpClient -import java.net.http.HttpRequest -import java.net.http.HttpResponse - -fun auth(): String { - val data: Map = mapOf( - "Email" to dotEnv["EMAIL"], - "Password" to dotEnv["PASSWORD"], - "Pin" to "", - "Role" to "" - ) - val req = HttpRequest.newBuilder() - .uri(URI.create("https://www.sshxl.nl/api/portal/ApiLogin")) - .header("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:138.0) Gecko/20100101 Firefox/138.0") - .header("Accept", "*/*") - .header("Content-Type", "application/json") - .header("Accept-Language", "en-US,en;q=0.5") - .header( - "Cookie", - listOf( - "cookie_consent_analytics=no", - "cookie_consent=no", - "SSHContext=${dotEnv["AUTH"]}" - ).joinToString("; ") - ) - .POST(HttpRequest.BodyPublishers.ofString(Json.encodeToString(data))) - .build() - - val client = HttpClient.newBuilder() - .followRedirects(HttpClient.Redirect.NORMAL) - .build() - - val res = client.send(req, HttpResponse.BodyHandlers.ofString()) - - if (res.statusCode() != 200) { - throw Exception("auth failed with status code ${res.statusCode()}") - } - - return Json.decodeFromString(res.body()).session -} - -@JsonIgnoreUnknownKeys -@Serializable -private data class Auth(val session: String) \ No newline at end of file diff --git a/src/main/kotlin/getEtage.kt b/src/main/kotlin/getEtage.kt deleted file mode 100644 index eadca11..0000000 --- a/src/main/kotlin/getEtage.kt +++ /dev/null @@ -1,40 +0,0 @@ -package me.koendev - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.Json -import me.koendev.utils.println - -@Serializable -data class Etages( - @SerialName("value") val offers: List, - @SerialName("@odata.count") val count: Int, - @SerialName("isComplete") val isComplete: Boolean -) - -@Serializable -data class Etage( - @SerialName("EtageWocasId") val etageWocasID: String, - @SerialName("Id") val etageID: Int, - @SerialName("Etage_EtagePhoto") val photos: List -) - -@Serializable -data class EtagePhoto( - @SerialName("Id") val id: Int, - @SerialName("EtageId") val etageID: Int, - @SerialName("EtagePhotoId") val etagePhotoID: Int, - @SerialName("EtagePhoto") val etagePhoto: List, -) - -@Serializable -data class EtagePhotoItem( - @SerialName("Id") val id: Int, - @SerialName("Photo") val url: String -) - -fun getEtage(id: String): Etage? { - val response = getEndpoint("OData/Etage?\$filter=(EtageWocasId%20eq%20'$id')&\$expand=Etage_EtagePhoto!(\$select=Id,EtagePhotoId,EtageId;\$expand=EtagePhoto!(\$select=Id,Photo))&\$select=Id,EtageWocasId") - - return Json.decodeFromString(response).offers.firstOrNull() -} diff --git a/src/main/kotlin/getFloorInfo.kt b/src/main/kotlin/getFloorInfo.kt index a4ee45a..2b49e4f 100644 --- a/src/main/kotlin/getFloorInfo.kt +++ b/src/main/kotlin/getFloorInfo.kt @@ -10,7 +10,7 @@ data class FloorInfo( @SerialName("Description") val description: String?, @SerialName("HospiteerDate") val hospiteerDate: String?, @SerialName("PreferenceSmokingAllowed") val smokingAllowed: Boolean?, - @SerialName("PreferencePetsAllowed") val petsAllowed: Boolean?, + @SerialName("PreferencePetsAllowed") val petsAllowed: Boolean, @SerialName("PreferenceGender") val genderPreference: String, @SerialName("NumberOfUnits") val numberOfUnits: Int, ) @@ -41,8 +41,8 @@ data class Floor( @SerialName("Kind") val kind: String, ) -fun getFloorInfo(room: Room, session: String): Floor { - val response = getEndpoint("offer/${room.flowId}", session) +fun getFloorInfo(room: Room): Floor { + val response = getEndpoint("offer/${room.flowId}") return Json.decodeFromString(response) } diff --git a/src/main/kotlin/getOffers.kt b/src/main/kotlin/getOffers.kt index ddaff4a..a6f4982 100644 --- a/src/main/kotlin/getOffers.kt +++ b/src/main/kotlin/getOffers.kt @@ -3,7 +3,6 @@ package me.koendev import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json -import me.koendev.utils.println @Serializable data class Offers( diff --git a/src/main/kotlin/requestTemplate.kt b/src/main/kotlin/requestTemplate.kt index 8383e89..5d82fde 100644 --- a/src/main/kotlin/requestTemplate.kt +++ b/src/main/kotlin/requestTemplate.kt @@ -4,15 +4,13 @@ import java.net.URI import java.net.http.HttpClient import java.net.http.HttpRequest import java.net.http.HttpResponse -import kotlinx.serialization.json.Json -import me.koendev.utils.println /** * @param path the endpoint to GET. `sshxl.nl/api/v1/` * * @return a HttpRequest with all the necessary properties to GET the endpoint. */ -fun buildRequest(path: String, session: String): HttpRequest.Builder { +fun buildRequest(path: String): HttpRequest { return HttpRequest.newBuilder() .uri(URI("https://www.sshxl.nl/api/v1/$path")) .header("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:138.0) Gecko/20100101 Firefox/138.0") @@ -23,27 +21,19 @@ fun buildRequest(path: String, session: String): HttpRequest.Builder { listOf( "cookie_consent_analytics=no", "cookie_consent=no", - "SSHContext=$session" + "SSHContext=${dotEnv["AUTH"]}" ).joinToString("; ") ) + .GET() + .build() } -fun getEndpoint(endpoint: String, session: String = ""): String { +fun getEndpoint(endpoint: String): String { val client = HttpClient.newBuilder() .followRedirects(HttpClient.Redirect.NORMAL) .build() - val request = buildRequest(endpoint, session) + val request = buildRequest(endpoint) - return client.send(request.GET().build(), HttpResponse.BodyHandlers.ofString()).body() -} - -fun postEndpoint(endpoint: String, data: Map, session: String = ""): String { - val client = HttpClient.newBuilder() - .followRedirects(HttpClient.Redirect.NORMAL) - .build() - - val request = buildRequest(endpoint, session) - - return client.send(request.POST(HttpRequest.BodyPublishers.ofString(Json.encodeToString(data))).build(), HttpResponse.BodyHandlers.ofString()).println().body() -} + return client.send(request, HttpResponse.BodyHandlers.ofString()).body() +} \ No newline at end of file