Compare commits
10 commits
cef71a5abe
...
0bfb3a28f2
| Author | SHA1 | Date | |
|---|---|---|---|
| 0bfb3a28f2 | |||
| 09c3d8cf37 | |||
| 207dce7113 | |||
| dad6a8557d | |||
| acbcc014ae | |||
| 2ebc5a8e00 | |||
| d790531583 | |||
| f9266ee695 | |||
| 745a4014a5 | |||
| c5f1fe6408 |
12 changed files with 200 additions and 69 deletions
BIN
KamerZoeken-1.0.0.jar
Normal file
BIN
KamerZoeken-1.0.0.jar
Normal file
Binary file not shown.
|
|
@ -7,12 +7,10 @@ If you want a command that just downloads, compiles and runs everything, run thi
|
||||||
```sh
|
```sh
|
||||||
git clone https://github.com/KoenDR06/KamerZoeken
|
git clone https://github.com/KoenDR06/KamerZoeken
|
||||||
cd KamerZoeken
|
cd KamerZoeken
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Then, set your preferences in [config.toml](config.toml). After you have done this, run the following script:
|
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:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
./gradlew build
|
java -jar KamerZoeken-1.0.0.jar
|
||||||
java -jar build/libs/KamerZoeken-1.0.0.jar
|
|
||||||
```
|
```
|
||||||
|
|
@ -15,6 +15,7 @@ dependencies {
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.1")
|
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.1")
|
||||||
implementation("de.thelooter:toml4j:0.8.1")
|
implementation("de.thelooter:toml4j:0.8.1")
|
||||||
implementation("io.github.cdimascio:dotenv-kotlin:6.5.1")
|
implementation("io.github.cdimascio:dotenv-kotlin:6.5.1")
|
||||||
|
implementation(files("../Utils-latest.jar"))
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.test {
|
tasks.test {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[general]
|
[general]
|
||||||
unitType = "Kamer" # Kamer of Woning
|
unitType = "Kamer" # Kamer of Woning
|
||||||
allowZeist = true # Of je ook in Zeist wil zoeken
|
cities = ["ZEIST", "UTRECHT"] # Steden om in te zoeken
|
||||||
|
|
||||||
smoking = -1 # 0: geen voorkeur, -1: ik wil dat er niet gerookt mag worden, 1: Ik wil dat er gerookt mag worden
|
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
|
pets = 0 # 0: geen voorkeur, -1: ik wil dat er geen huisdieren mogen, 1: Ik wil dat huisdieren mogen
|
||||||
|
|
|
||||||
|
|
@ -1 +1,2 @@
|
||||||
AUTH=
|
EMAIL=test@example.com
|
||||||
|
PASSWORD="123456789"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package me.koendev
|
package me.koendev
|
||||||
|
|
||||||
import io.github.cdimascio.dotenv.Dotenv
|
import io.github.cdimascio.dotenv.Dotenv
|
||||||
|
import me.koendev.utils.println
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
|
|
@ -14,43 +15,63 @@ data class ReactableOffer(
|
||||||
val floor: Floor
|
val floor: Floor
|
||||||
)
|
)
|
||||||
|
|
||||||
fun main() {
|
fun main(args: Array<String>) {
|
||||||
|
val headless = args.size == 1 && args[0] == "headless"
|
||||||
|
|
||||||
|
if (!headless) print("Authenticating")
|
||||||
|
val sessionToken = auth()
|
||||||
|
|
||||||
|
if (!headless) print("\rGetting rooms ")
|
||||||
val rooms = getRooms().filter { room ->
|
val rooms = getRooms().filter { room ->
|
||||||
room.unitType == config.general.unitType
|
room.unitType == config.general.unitType
|
||||||
}
|
}
|
||||||
if (rooms.isEmpty()) {
|
if (rooms.isEmpty()) {
|
||||||
println("No suitable offers were found, quitting.")
|
System.err.println("No suitable offers were found, quitting.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val offers = getOffers(rooms.map { it.wocasId }).offers.filter { offer ->
|
val offers = getOffers(rooms.map { it.wocasId }).offers.filter { offer ->
|
||||||
offer.adres[0].plaats in listOf(
|
offer.adres[0].plaats in config.general.cities
|
||||||
"UTRECHT",
|
|
||||||
) + if (config.general.allowZeist) "ZEIST" else ""
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var index = 0
|
||||||
val coupled = rooms.mapNotNull { room ->
|
val coupled = rooms.mapNotNull { room ->
|
||||||
val offer: Offer? = offers.find { room.wocasId.toInt() == it.eenheidNummer.toInt() }
|
val offer: Offer? = offers.find { room.wocasId.toInt() == it.eenheidNummer.toInt() }
|
||||||
|
|
||||||
if (offer == null) null else ReactableOffer(room, offer, getFloorInfo(room))
|
if (!headless) print("\rFiltering per room: ${index++} / ${rooms.size}")
|
||||||
|
if (offer == null) null else ReactableOffer(room, offer, getFloorInfo(room, sessionToken))
|
||||||
}.filter {
|
}.filter {
|
||||||
val gender = it.floor.floorInfo.genderPreference
|
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 date = it.room.expireBy.take(10)
|
||||||
val date1 = LocalDate.now()
|
val date1 = LocalDate.now()
|
||||||
val date2 = LocalDate.parse(date, DateTimeFormatter.ofPattern("yyyy-MM-dd"))
|
val date2 = LocalDate.parse(date, DateTimeFormatter.ofPattern("yyyy-MM-dd"))
|
||||||
val daysLeft = ChronoUnit.DAYS.between(date1, date2)
|
|
||||||
|
val days = ChronoUnit.DAYS.between(date1, date2)
|
||||||
|
|
||||||
((gender == "female" && config.gender.female) ||
|
((gender == "female" && config.gender.female) ||
|
||||||
(gender == "male" && config.gender.male) ||
|
(gender == "male" && config.gender.male) ||
|
||||||
(gender == "none" && config.gender.none)) &&
|
(gender == "none" && config.gender.none)) &&
|
||||||
|
|
||||||
((config.general.smoking == -1 && !(it.floor.floorInfo.smokingAllowed ?: true)) || (config.general.smoking == 1 && it.floor.floorInfo.smokingAllowed ?: true) || config.general.smoking == 0) &&
|
((config.general.smoking == -1 && !smoking) || (config.general.smoking == 1 && smoking) || config.general.smoking == 0) &&
|
||||||
|
|
||||||
((config.general.pets == -1 && !it.floor.floorInfo.petsAllowed) || (config.general.pets == 1 && it.floor.floorInfo.petsAllowed) || config.general.pets == 0) &&
|
((config.general.pets == -1 && !pets) || (config.general.pets == 1 && pets) || config.general.pets == 0) &&
|
||||||
|
|
||||||
daysLeft == 0L
|
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"))
|
||||||
|
|
||||||
|
ChronoUnit.DAYS.between(date1, date2)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!headless) {
|
||||||
val fileName = "offers.md"
|
val fileName = "offers.md"
|
||||||
val out = File(fileName)
|
val out = File(fileName)
|
||||||
if (!out.exists()) out.createNewFile()
|
if (!out.exists()) out.createNewFile()
|
||||||
|
|
@ -60,7 +81,7 @@ fun main() {
|
||||||
coupled.forEach {
|
coupled.forEach {
|
||||||
val address = it.offer.adres[0]
|
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("## [${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| Categorie | Waarde |\n")
|
||||||
str.append("|-------------|--------------------|\n")
|
str.append("|-------------|--------------------|\n")
|
||||||
|
|
@ -75,10 +96,10 @@ fun main() {
|
||||||
}
|
}
|
||||||
str.append("| Geslacht | ${genderString.padEnd(18, ' ')} |\n")
|
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("| 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("| Huisdieren | ${(if (it.floor.floorInfo.petsAllowed ?: false) "✅ Mogen" else "❌ Mogen niet").padEnd(17, ' ')} |\n")
|
||||||
str.append("| Reacties | ${it.floor.potentialPosition} van de ${it.floor.applicantCount.toString().padStart(3, ' ')}. |\n")
|
val positionString = "${it.floor.potentialPosition} / ${it.floor.applicantCount}."
|
||||||
|
str.append("| Reacties | ${positionString.padEnd(18, ' ')} |\n")
|
||||||
|
|
||||||
val date = it.room.expireBy.take(10)
|
val date = it.room.expireBy.take(10)
|
||||||
val date1 = LocalDate.now()
|
val date1 = LocalDate.now()
|
||||||
|
|
@ -87,10 +108,18 @@ fun main() {
|
||||||
str.append("| Tijd over | $daysLeft dagen over. |\n")
|
str.append("| Tijd over | $daysLeft dagen over. |\n")
|
||||||
|
|
||||||
str.append("\n")
|
str.append("\n")
|
||||||
|
|
||||||
str.append("### Message: \n\n${it.floor.floorInfo.description ?: "Deze pannekoeken hebben geen bericht achtergelaten"}\n")
|
str.append("### Message: \n\n${it.floor.floorInfo.description ?: "Deze pannekoeken hebben geen bericht achtergelaten"}\n")
|
||||||
|
|
||||||
str.append("\n\n")
|
str.append("\n\n")
|
||||||
}
|
}
|
||||||
out.writeText(str.toString())
|
out.writeText(str.toString())
|
||||||
println("${coupled.size} offers found, wrote to $fileName")
|
println("\r${coupled.size} offers found, wrote to $fileName")
|
||||||
|
} else {
|
||||||
|
coupled.filter {
|
||||||
|
it.floor.potentialPosition <= 20
|
||||||
|
}.forEach {
|
||||||
|
postEndpoint("reactions/${it.room.flowId}", mapOf(), sessionToken)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import java.io.File
|
||||||
|
|
||||||
data class GeneralConfig(
|
data class GeneralConfig(
|
||||||
val unitType: String,
|
val unitType: String,
|
||||||
val allowZeist: Boolean,
|
val cities: List<String>,
|
||||||
val smoking: Int,
|
val smoking: Int,
|
||||||
val pets: Int
|
val pets: Int
|
||||||
)
|
)
|
||||||
|
|
|
||||||
51
src/main/kotlin/authenticate.kt
Normal file
51
src/main/kotlin/authenticate.kt
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
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<String, String> = 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<Auth>(res.body()).session
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonIgnoreUnknownKeys
|
||||||
|
@Serializable
|
||||||
|
private data class Auth(val session: String)
|
||||||
40
src/main/kotlin/getEtage.kt
Normal file
40
src/main/kotlin/getEtage.kt
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
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<Etage>,
|
||||||
|
@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<EtagePhoto>
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class EtagePhoto(
|
||||||
|
@SerialName("Id") val id: Int,
|
||||||
|
@SerialName("EtageId") val etageID: Int,
|
||||||
|
@SerialName("EtagePhotoId") val etagePhotoID: Int,
|
||||||
|
@SerialName("EtagePhoto") val etagePhoto: List<EtagePhotoItem>,
|
||||||
|
)
|
||||||
|
|
||||||
|
@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<Etages>(response).offers.firstOrNull()
|
||||||
|
}
|
||||||
|
|
@ -10,7 +10,7 @@ data class FloorInfo(
|
||||||
@SerialName("Description") val description: String?,
|
@SerialName("Description") val description: String?,
|
||||||
@SerialName("HospiteerDate") val hospiteerDate: String?,
|
@SerialName("HospiteerDate") val hospiteerDate: String?,
|
||||||
@SerialName("PreferenceSmokingAllowed") val smokingAllowed: Boolean?,
|
@SerialName("PreferenceSmokingAllowed") val smokingAllowed: Boolean?,
|
||||||
@SerialName("PreferencePetsAllowed") val petsAllowed: Boolean,
|
@SerialName("PreferencePetsAllowed") val petsAllowed: Boolean?,
|
||||||
@SerialName("PreferenceGender") val genderPreference: String,
|
@SerialName("PreferenceGender") val genderPreference: String,
|
||||||
@SerialName("NumberOfUnits") val numberOfUnits: Int,
|
@SerialName("NumberOfUnits") val numberOfUnits: Int,
|
||||||
)
|
)
|
||||||
|
|
@ -41,8 +41,8 @@ data class Floor(
|
||||||
@SerialName("Kind") val kind: String,
|
@SerialName("Kind") val kind: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun getFloorInfo(room: Room): Floor {
|
fun getFloorInfo(room: Room, session: String): Floor {
|
||||||
val response = getEndpoint("offer/${room.flowId}")
|
val response = getEndpoint("offer/${room.flowId}", session)
|
||||||
|
|
||||||
return Json.decodeFromString<Floor>(response)
|
return Json.decodeFromString<Floor>(response)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package me.koendev
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
import me.koendev.utils.println
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Offers(
|
data class Offers(
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,15 @@ import java.net.URI
|
||||||
import java.net.http.HttpClient
|
import java.net.http.HttpClient
|
||||||
import java.net.http.HttpRequest
|
import java.net.http.HttpRequest
|
||||||
import java.net.http.HttpResponse
|
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/<PATH>`
|
* @param path the endpoint to GET. `sshxl.nl/api/v1/<PATH>`
|
||||||
*
|
*
|
||||||
* @return a HttpRequest with all the necessary properties to GET the endpoint.
|
* @return a HttpRequest with all the necessary properties to GET the endpoint.
|
||||||
*/
|
*/
|
||||||
fun buildRequest(path: String): HttpRequest {
|
fun buildRequest(path: String, session: String): HttpRequest.Builder {
|
||||||
return HttpRequest.newBuilder()
|
return HttpRequest.newBuilder()
|
||||||
.uri(URI("https://www.sshxl.nl/api/v1/$path"))
|
.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")
|
.header("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:138.0) Gecko/20100101 Firefox/138.0")
|
||||||
|
|
@ -21,19 +23,27 @@ fun buildRequest(path: String): HttpRequest {
|
||||||
listOf(
|
listOf(
|
||||||
"cookie_consent_analytics=no",
|
"cookie_consent_analytics=no",
|
||||||
"cookie_consent=no",
|
"cookie_consent=no",
|
||||||
"SSHContext=${dotEnv["AUTH"]}"
|
"SSHContext=$session"
|
||||||
).joinToString("; ")
|
).joinToString("; ")
|
||||||
)
|
)
|
||||||
.GET()
|
|
||||||
.build()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getEndpoint(endpoint: String): String {
|
fun getEndpoint(endpoint: String, session: String = ""): String {
|
||||||
val client = HttpClient.newBuilder()
|
val client = HttpClient.newBuilder()
|
||||||
.followRedirects(HttpClient.Redirect.NORMAL)
|
.followRedirects(HttpClient.Redirect.NORMAL)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val request = buildRequest(endpoint)
|
val request = buildRequest(endpoint, session)
|
||||||
|
|
||||||
return client.send(request, HttpResponse.BodyHandlers.ofString()).body()
|
return client.send(request.GET().build(), HttpResponse.BodyHandlers.ofString()).body()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun postEndpoint(endpoint: String, data: Map<String, String>, 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()
|
||||||
}
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue