Add dynamic endpoints response json keys mapping

This commit is contained in:
Mikołaj Pich 2024-05-30 15:32:15 +02:00
parent be44f3b25c
commit 7f9456fe6c
No known key found for this signature in database
7 changed files with 144 additions and 9 deletions

View file

@ -69,6 +69,7 @@ public final class io/github/wulkanowy/sdk/scrapper/Scrapper {
public static synthetic fun getReceivedMessages$default (Lio/github/wulkanowy/sdk/scrapper/Scrapper;Ljava/lang/String;IILkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; public static synthetic fun getReceivedMessages$default (Lio/github/wulkanowy/sdk/scrapper/Scrapper;Ljava/lang/String;IILkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
public final fun getRecipients (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public final fun getRecipients (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public final fun getRegisteredDevices (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public final fun getRegisteredDevices (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public final fun getResponseMapping ()Ljava/util/Map;
public final fun getSchool (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public final fun getSchool (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public final fun getSchoolId ()Ljava/lang/String; public final fun getSchoolId ()Ljava/lang/String;
public final fun getSchoolYear ()I public final fun getSchoolYear ()I
@ -116,6 +117,7 @@ public final class io/github/wulkanowy/sdk/scrapper/Scrapper {
public final fun setLogLevel (Lokhttp3/logging/HttpLoggingInterceptor$Level;)V public final fun setLogLevel (Lokhttp3/logging/HttpLoggingInterceptor$Level;)V
public final fun setLoginType (Lio/github/wulkanowy/sdk/scrapper/Scrapper$LoginType;)V public final fun setLoginType (Lio/github/wulkanowy/sdk/scrapper/Scrapper$LoginType;)V
public final fun setPassword (Ljava/lang/String;)V public final fun setPassword (Ljava/lang/String;)V
public final fun setResponseMapping (Ljava/util/Map;)V
public final fun setSchoolId (Ljava/lang/String;)V public final fun setSchoolId (Ljava/lang/String;)V
public final fun setSchoolYear (I)V public final fun setSchoolYear (I)V
public final fun setSsl (Z)V public final fun setSsl (Z)V

View file

@ -811,6 +811,47 @@ internal val ApiEndpointsVTokenMap = mapOf(
), ),
) )
internal val ApiEndpointsResponseMapping = mapOf(
"24.04.0010.58863" to mapOf(
"uonetplus-wiadomosciplus" to mapOf(
"__common__" to mapOf(
"apiGlobalKey" to "rtvrHBuCAwCEEAtIsBtuHJBtFttEtCIJ",
"data" to "GFBGBsJGJFHvErwAswuGruutHHtuHEuG",
"hasZalaczniki" to "IDJArEArvBsIErGDrsuEuGssHwsHGEts",
"id" to "IDwrCCJDGFrHEuIFsvEvDDJwtBFECCHJ",
"korespondenci" to "AurrDtEJwwrEEwJtIJvEHHwICuBBDFGF",
"nieprzeczytanePrzeczytanePrzez" to "DBtuuvuGvEBsEAHAJrJECsDGuFrFsuGw",
"przeczytana" to "FuBsJwBvHErtEDAwJGuHCuIHJwBrrJFI",
"skrzynka" to "AuvBvruBGtGvEJHtIrsGvrIBDGAIFJCB",
"temat" to "rtBCHvtsICwDEEFuJwvwJDCJBIAvAGCv",
"uzytkownikRola" to "twrCEIrHrsuGEIFIsCGEDJHDwrCICwBG",
"wazna" to "AtAsGsuEIAurEGBAJBCGIIFsuwFrwsJt",
"wycofana" to "twsrwAIrvGJFEutCrBrDvHCGwGHGCAGv",
),
"Skrzynki" to mapOf(
"globalKey" to "uwtBBGEJtGHCEEvvsuCJEtCJHCrustHu",
"nazwa" to "svttEIFJuuvGECBBrGCGtuuBFJBCAtGE",
"typUzytkownika" to "BAICCDrJHtAEEtABrJFuFtvGuICrrCGC",
),
"WiadomoscSzczegoly" to mapOf(
"apiGlobalKey" to "IvDDwGIurwIrEHDHIBvAvBEBvCsstBCC",
"data" to "utFsEtBJsrEuEECBJDuuIEsvFDCsEIuv",
"dataWycofania" to "DDAtuGHJBIEFErBArstDrsBAvIsvBHHs",
"id" to "vFICGIHIAwvBEwIHrvstBvwvCJJIGwJE",
"nadawca" to "DuJuEEvwFwAFEsAErCEtuIBtvwDsJutA",
"nadawcaInfo" to "DDAtuGHJBIEFErBArstDrsBAvIsvBHHs",
"nadawcaTyp" to "DGswFwrIGwFsEEFFrBDwvsrIsFHHCGst",
"odbiorcy" to "srBIECHwtJEuEJFIsBHBGsGIrHuBCJAu",
"odczytana" to "wwArFDurBrDFEvrAJGrsIIFADCswFuIE",
"temat" to "wuEwstuHDJCGEuEvsBJrEGFvIwvuvCJC",
"tresc" to "BEDvtCIEBCGJEDGwswIFIvIuAHFDrAJI",
"wycofana" to "HrvHItBsstFrEABGIDtuuGJJIGBECwFu",
"zalaczniki" to "GvtvvDBGvsAHEBsDsBJIJGtAtCvFswAI",
),
),
),
)
internal val ApiEndpointsVHeaders = mapOf( internal val ApiEndpointsVHeaders = mapOf(
"24.04.0003.58698" to mapOf( "24.04.0003.58698" to mapOf(
"uonetplus-wiadomosciplus" to mapOf( "uonetplus-wiadomosciplus" to mapOf(

View file

@ -230,6 +230,12 @@ class Scrapper {
vHeadersMap = value vHeadersMap = value
} }
var responseMapping: Map<String, Map<String, Map<String, Map<String, String>>>>
get() = responseMap
set(value) {
responseMap = value
}
var vParamsEvaluation: suspend () -> EvaluateHandler var vParamsEvaluation: suspend () -> EvaluateHandler
get() = vParamsRun get() = vParamsRun
set(value) { set(value) {
@ -240,6 +246,7 @@ class Scrapper {
var endpointsMap: Map<String, Map<String, Map<String, String>>> = ApiEndpointsMap var endpointsMap: Map<String, Map<String, Map<String, String>>> = ApiEndpointsMap
var vTokenMap: Map<String, Map<String, Map<String, String>>> = ApiEndpointsVTokenMap var vTokenMap: Map<String, Map<String, Map<String, String>>> = ApiEndpointsVTokenMap
var vHeadersMap: Map<String, Map<String, Map<String, String>>> = ApiEndpointsVHeaders var vHeadersMap: Map<String, Map<String, Map<String, String>>> = ApiEndpointsVHeaders
var responseMap: Map<String, Map<String, Map<String, Map<String, String>>>> = ApiEndpointsResponseMapping
var vParamsRun: suspend () -> EvaluateHandler = { object : EvaluateHandler {} } var vParamsRun: suspend () -> EvaluateHandler = { object : EvaluateHandler {} }
} }

View file

@ -332,6 +332,15 @@ internal suspend fun getModuleHeadersFromDocument(document: Document): ModuleHea
) )
} }
internal fun getModuleHost(url: HttpUrl): String {
return when {
MessagesModuleHost in url.host -> MessagesModuleHost
StudentPlusModuleHost in url.host -> StudentPlusModuleHost
StudentModuleHost in url.host -> StudentModuleHost
else -> ""
}
}
internal fun getVHeaders(moduleHost: String, url: HttpUrl, headers: ModuleHeaders?): Map<String, String> { internal fun getVHeaders(moduleHost: String, url: HttpUrl, headers: ModuleHeaders?): Map<String, String> {
val vHeaders = Scrapper.vHeadersMap[headers?.appVersion] ?: ApiEndpointsVHeaders[headers?.appVersion] val vHeaders = Scrapper.vHeadersMap[headers?.appVersion] ?: ApiEndpointsVHeaders[headers?.appVersion]

View file

@ -1,7 +1,9 @@
package io.github.wulkanowy.sdk.scrapper.interceptor package io.github.wulkanowy.sdk.scrapper.interceptor
import io.github.wulkanowy.sdk.scrapper.ApiEndpointsResponseMapping
import io.github.wulkanowy.sdk.scrapper.ApiResponse import io.github.wulkanowy.sdk.scrapper.ApiResponse
import io.github.wulkanowy.sdk.scrapper.CookieJarCabinet import io.github.wulkanowy.sdk.scrapper.CookieJarCabinet
import io.github.wulkanowy.sdk.scrapper.Scrapper
import io.github.wulkanowy.sdk.scrapper.Scrapper.LoginType import io.github.wulkanowy.sdk.scrapper.Scrapper.LoginType
import io.github.wulkanowy.sdk.scrapper.Scrapper.LoginType.ADFS import io.github.wulkanowy.sdk.scrapper.Scrapper.LoginType.ADFS
import io.github.wulkanowy.sdk.scrapper.Scrapper.LoginType.ADFSCards import io.github.wulkanowy.sdk.scrapper.Scrapper.LoginType.ADFSCards
@ -12,6 +14,8 @@ import io.github.wulkanowy.sdk.scrapper.Scrapper.LoginType.STANDARD
import io.github.wulkanowy.sdk.scrapper.exception.VulcanClientError import io.github.wulkanowy.sdk.scrapper.exception.VulcanClientError
import io.github.wulkanowy.sdk.scrapper.exception.VulcanServerError import io.github.wulkanowy.sdk.scrapper.exception.VulcanServerError
import io.github.wulkanowy.sdk.scrapper.getModuleHeadersFromDocument import io.github.wulkanowy.sdk.scrapper.getModuleHeadersFromDocument
import io.github.wulkanowy.sdk.scrapper.getModuleHost
import io.github.wulkanowy.sdk.scrapper.getPathIndexByModuleHost
import io.github.wulkanowy.sdk.scrapper.getVHeaders import io.github.wulkanowy.sdk.scrapper.getVHeaders
import io.github.wulkanowy.sdk.scrapper.isAnyMappingAvailable import io.github.wulkanowy.sdk.scrapper.isAnyMappingAvailable
import io.github.wulkanowy.sdk.scrapper.login.LoginModuleResult import io.github.wulkanowy.sdk.scrapper.login.LoginModuleResult
@ -26,6 +30,11 @@ import io.github.wulkanowy.sdk.scrapper.repository.AccountRepository.Companion.S
import io.github.wulkanowy.sdk.scrapper.repository.AccountRepository.Companion.SELECTOR_STANDARD import io.github.wulkanowy.sdk.scrapper.repository.AccountRepository.Companion.SELECTOR_STANDARD
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import okhttp3.HttpUrl import okhttp3.HttpUrl
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.MediaType import okhttp3.MediaType
@ -33,8 +42,10 @@ import okhttp3.Protocol
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import okhttp3.ResponseBody import okhttp3.ResponseBody
import okhttp3.ResponseBody.Companion.toResponseBody
import okio.Buffer import okio.Buffer
import okio.BufferedSource import okio.BufferedSource
import okio.use
import org.jsoup.Jsoup import org.jsoup.Jsoup
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import org.jsoup.select.Elements import org.jsoup.select.Elements
@ -72,7 +83,7 @@ internal class AutoLoginInterceptor(
val request = chain.request() val request = chain.request()
checkRequest() checkRequest()
val response = try { val response = try {
chain.proceed(request.attachModuleHeaders()) performRequest(chain, request)
} catch (e: Throwable) { } catch (e: Throwable) {
when (e) { when (e) {
is VulcanClientError -> checkHttpErrorResponse(e, url) is VulcanClientError -> checkHttpErrorResponse(e, url)
@ -80,12 +91,6 @@ internal class AutoLoginInterceptor(
} }
throw e throw e
} }
if (response.body?.contentType()?.subtype != "json") {
val body = response.peekBody(Long.MAX_VALUE).byteStream()
val html = Jsoup.parse(body, null, url)
checkResponse(html, url, response)
saveModuleHeaders(html, uri)
}
response response
} catch (e: NotLoggedInException) { } catch (e: NotLoggedInException) {
if (loginLock.tryLock()) { if (loginLock.tryLock()) {
@ -109,7 +114,7 @@ internal class AutoLoginInterceptor(
StudentModuleHost in uri.host -> student.getOrThrow() StudentModuleHost in uri.host -> student.getOrThrow()
else -> logger.info("Resource don't need further login anyway") else -> logger.info("Resource don't need further login anyway")
} }
chain.proceed(chain.request().attachModuleHeaders()) performRequest(chain, chain.request())
} catch (e: IOException) { } catch (e: IOException) {
logger.debug("IO Error occurred on login") logger.debug("IO Error occurred on login")
throw e throw e
@ -131,7 +136,7 @@ internal class AutoLoginInterceptor(
logger.debug("User logged in. Retry after login...") logger.debug("User logged in. Retry after login...")
} }
chain.proceed(chain.request().attachModuleHeaders()) performRequest(chain, chain.request())
} }
} }
} }
@ -190,6 +195,69 @@ internal class AutoLoginInterceptor(
.build() .build()
} }
private fun performRequest(chain: Interceptor.Chain, request: Request): Response {
val response = chain.proceed(request.attachModuleHeaders())
val url = request.url.toString()
val uri = request.url
return if (response.body?.contentType()?.subtype != "json") {
val body = response.peekBody(Long.MAX_VALUE).byteStream()
val html = Jsoup.parse(body, null, url)
checkResponse(html, url, response)
saveModuleHeaders(html, uri)
response
} else {
handleResponseMapping(response, uri)
}
}
private fun handleResponseMapping(response: Response, uri: HttpUrl): Response {
val moduleHost = getModuleHost(uri)
val pathSegmentIndex = getPathIndexByModuleHost(moduleHost)
val pathKey = uri.pathSegments.getOrNull(pathSegmentIndex)
val headers = headersByHost[moduleHost]
val mappings = Scrapper.responseMap[headers?.appVersion]?.get(moduleHost)
?: ApiEndpointsResponseMapping[headers?.appVersion]?.get(moduleHost)
if (mappings.isNullOrEmpty()) return response
val jsonMappings = mappings[pathKey] ?: mappings["__common__"]
return response.body?.byteStream()?.bufferedReader()?.use {
val contentType = response.body?.contentType()
val body = mapResponseContent(it.readText(), jsonMappings).toResponseBody(contentType)
response.newBuilder().body(body).build()
} ?: response
}
private fun mapResponseContent(input: String, jsonMappings: Map<String, String>?): String {
return when (val response = Json.decodeFromString<JsonElement>(input)) {
is JsonArray -> JsonArray(
response.jsonArray.map {
when (it) {
is JsonArray -> it.jsonArray
is JsonObject -> mapJsonObjectKeys(it.jsonObject, jsonMappings)
else -> it
}
},
)
is JsonObject -> mapJsonObjectKeys(response.jsonObject, jsonMappings)
else -> response
}.toString()
}
private fun mapJsonObjectKeys(jsonObject: JsonObject, jsonMappings: Map<String, String>?): JsonObject {
val mapping = jsonMappings?.map { (key, value) ->
value to key
}.orEmpty().toMap()
return JsonObject(
jsonObject.mapKeys {
mapping[it.key] ?: it.key
},
)
}
private fun checkRequest() { private fun checkRequest() {
if (emptyCookieJarIntercept && !cookieJarCabinet.isUserCookiesExist()) { if (emptyCookieJarIntercept && !cookieJarCabinet.isUserCookiesExist()) {
throw NotLoggedInException("No cookie found! You are not logged in yet") throw NotLoggedInException("No cookie found! You are not logged in yet")

View file

@ -62,6 +62,7 @@ public final class io/github/wulkanowy/sdk/Sdk {
public static synthetic fun getReceivedMessages$default (Lio/github/wulkanowy/sdk/Sdk;Ljava/lang/String;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; public static synthetic fun getReceivedMessages$default (Lio/github/wulkanowy/sdk/Sdk;Ljava/lang/String;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
public final fun getRecipients (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public final fun getRecipients (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public final fun getRegisteredDevices (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public final fun getRegisteredDevices (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public final fun getResponseMapping ()Ljava/util/Map;
public final fun getSchool (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public final fun getSchool (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public final fun getSchoolSymbol ()Ljava/lang/String; public final fun getSchoolSymbol ()Ljava/lang/String;
public final fun getSchoolYear ()I public final fun getSchoolYear ()I
@ -116,6 +117,7 @@ public final class io/github/wulkanowy/sdk/Sdk {
public final fun setMode (Lio/github/wulkanowy/sdk/Sdk$Mode;)V public final fun setMode (Lio/github/wulkanowy/sdk/Sdk$Mode;)V
public final fun setPassword (Ljava/lang/String;)V public final fun setPassword (Ljava/lang/String;)V
public final fun setPrivatePem (Ljava/lang/String;)V public final fun setPrivatePem (Ljava/lang/String;)V
public final fun setResponseMapping (Ljava/util/Map;)V
public final fun setSchoolSymbol (Ljava/lang/String;)V public final fun setSchoolSymbol (Ljava/lang/String;)V
public final fun setSchoolYear (I)V public final fun setSchoolYear (I)V
public final fun setScrapperBaseUrl (Ljava/lang/String;)V public final fun setScrapperBaseUrl (Ljava/lang/String;)V

View file

@ -254,6 +254,12 @@ class Sdk {
scrapper.vHeaders = value scrapper.vHeaders = value
} }
var responseMapping
get() = scrapper.responseMapping
set(value) {
scrapper.responseMapping = value
}
var vParamsEvaluation: suspend () -> EvaluateHandler var vParamsEvaluation: suspend () -> EvaluateHandler
get() = scrapper.vParamsEvaluation get() = scrapper.vParamsEvaluation
set(value) { set(value) {