Add support for register via eduOne

This commit is contained in:
Mikołaj Pich 2024-03-19 00:33:55 +01:00
parent 79f55fba1d
commit 41aef1a68b
No known key found for this signature in database
14 changed files with 184 additions and 33 deletions

View file

@ -18,7 +18,7 @@ ext {
moshi = "1.13.0"
}
version = "2.5.1"
version = "2.5.2-SNAPSHOT"
group = "io.github.wulkanowy"
nexusPublishing {

View file

@ -126,6 +126,10 @@ public final class io/github/wulkanowy/sdk/scrapper/UtilsKt {
public static final fun getNormalizedSymbol (Ljava/lang/String;)Ljava/lang/String;
}
public synthetic class io/github/wulkanowy/sdk/scrapper/UtilsKt$EntriesMappings {
public static final synthetic field entries$0 Lkotlin/enums/EnumEntries;
}
public final class io/github/wulkanowy/sdk/scrapper/attendance/Absent {
public fun <init> (Ljava/time/LocalDateTime;Ljava/lang/Integer;)V
public final fun component1 ()Ljava/time/LocalDateTime;

View file

@ -273,6 +273,7 @@ class Scrapper {
),
register = serviceManager.getRegisterService(),
student = serviceManager.getStudentService(withLogin = false, studentInterceptor = false),
studentPlus = serviceManager.getStudentPlusService(withLogin = false, studentInterceptor = false),
symbolService = serviceManager.getSymbolService(),
url = serviceManager.urlGenerator,
)

View file

@ -1,5 +1,6 @@
package io.github.wulkanowy.sdk.scrapper
import io.github.wulkanowy.sdk.scrapper.login.UrlGenerator
import io.github.wulkanowy.sdk.scrapper.messages.Mailbox
import io.github.wulkanowy.sdk.scrapper.messages.Recipient
import io.github.wulkanowy.sdk.scrapper.messages.RecipientType
@ -14,6 +15,8 @@ import java.time.LocalDateTime
import java.time.ZoneId.systemDefault
import java.time.format.DateTimeFormatter.ofPattern
import java.util.Date
import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi
import kotlin.math.roundToInt
internal fun String.toDate(format: String): Date = SimpleDateFormat(format).parse(this)
@ -135,6 +138,39 @@ internal fun getFormattedString(
return String.format(template, androidVersion, buildTag, webKitRev, chromeRev, webKitRev)
}
internal fun isCurrentLoginHasEduOne(studentModuleUrls: List<String>, urlGenerator: UrlGenerator): Boolean {
return studentModuleUrls.any {
it.startsWith(
prefix = urlGenerator.generate(UrlGenerator.Site.STUDENT_PLUS),
ignoreCase = true,
)
}
}
@OptIn(ExperimentalEncodingApi::class)
internal fun getEncodedKey(studentId: Int, diaryId: Int, unitId: Int): String {
return Base64.encode("$studentId-$diaryId-1-$unitId".toByteArray())
}
@OptIn(ExperimentalEncodingApi::class)
internal fun getDecodedKey(key: String): StudentKey {
val parts = Base64.decode(key).decodeToString()
.split("-").map { it.toInt() }
return StudentKey(
studentId = parts[0],
diaryId = parts[1],
unknown = parts[2],
unitId = parts[3],
)
}
internal data class StudentKey(
val studentId: Int,
val diaryId: Int,
val unknown: Int,
val unitId: Int,
)
internal fun <T> Response<T>.handleErrors(): Response<T> {
if (!isSuccessful) {
throw HttpException(this)

View file

@ -0,0 +1,20 @@
package io.github.wulkanowy.sdk.scrapper.grades
import io.github.wulkanowy.sdk.scrapper.adapter.CustomDateAdapter
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import java.time.LocalDateTime
@Serializable
internal data class GradeSemester(
@Serializable(with = CustomDateAdapter::class)
@SerialName("dataDo")
val dataDo: LocalDateTime,
@SerialName("dataOd")
@Serializable(with = CustomDateAdapter::class)
val dataOd: LocalDateTime,
@SerialName("id")
val id: Int,
@SerialName("numerOkresu")
val numerOkresu: Int,
)

View file

@ -10,6 +10,7 @@ import io.github.wulkanowy.sdk.scrapper.Scrapper.LoginType.ADFSLightScoped
import io.github.wulkanowy.sdk.scrapper.Scrapper.LoginType.STANDARD
import io.github.wulkanowy.sdk.scrapper.exception.VulcanClientError
import io.github.wulkanowy.sdk.scrapper.getScriptParam
import io.github.wulkanowy.sdk.scrapper.isCurrentLoginHasEduOne
import io.github.wulkanowy.sdk.scrapper.login.ModuleHeaders
import io.github.wulkanowy.sdk.scrapper.login.NotLoggedInException
import io.github.wulkanowy.sdk.scrapper.login.UrlGenerator
@ -90,7 +91,7 @@ internal class AutoLoginInterceptor(
try {
val homePageResponse = runBlocking { notLoggedInCallback() }
val studentModuleUrls = homePageResponse.studentSchools.map { it.attr("href") }
val isEduOne = isCurrentLoginHasEduOne(studentModuleUrls)
val isEduOne = isCurrentLoginHasEduOne(studentModuleUrls, urlGenerator)
isEduOneStudent(isEduOne)
messagesModuleHeaders = null
studentPlusModuleHeaders = null
@ -175,15 +176,6 @@ internal class AutoLoginInterceptor(
}
}
private fun isCurrentLoginHasEduOne(studentModuleUrls: List<String>): Boolean {
return studentModuleUrls.any {
it.startsWith(
prefix = urlGenerator.generate(UrlGenerator.Site.STUDENT_PLUS),
ignoreCase = true,
)
}
}
private fun Request.attachModuleHeaders(): Request {
val headers = when {
"uonetplus-wiadomosciplus" in url.host -> messagesModuleHeaders

View file

@ -1,7 +1,9 @@
package io.github.wulkanowy.sdk.scrapper.register
import io.github.wulkanowy.sdk.scrapper.adapter.CustomDateAdapter
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import java.time.LocalDateTime
@Serializable
internal data class ContextResponse(
@ -13,10 +15,12 @@ internal data class ContextResponse(
internal data class ContextStudent(
@SerialName("config")
val config: ContextConfig,
@Serializable(with = CustomDateAdapter::class)
@SerialName("dziennikDataOd")
val registerDateFrom: String,
val registerDateFrom: LocalDateTime,
@Serializable(with = CustomDateAdapter::class)
@SerialName("dziennikDataDo")
val registerDateTo: String,
val registerDateTo: LocalDateTime,
@SerialName("globalKeySkrzynka")
val globalKeyMailbox: String,
@SerialName("idDziennik")

View file

@ -1,10 +1,8 @@
package io.github.wulkanowy.sdk.scrapper.register
import io.github.wulkanowy.sdk.scrapper.timetable.CacheResponse
internal fun getStudentsFromDiaries(
diaries: List<Diary>,
cache: CacheResponse?,
isParent: Boolean?,
unitId: Int,
): List<RegisterStudent> = diaries
.filter { it.semesters.orEmpty().isNotEmpty() || it.kindergartenDiaryId != 0 }
@ -19,7 +17,7 @@ internal fun getStudentsFromDiaries(
studentSurname = diary.studentSurname,
className = diary.symbol.orEmpty(),
classId = classId,
isParent = cache?.isParent == true,
isParent = isParent == true,
isAuthorized = diary.isAuthorized == true,
semesters = diaries.toSemesters(
studentId = diary.studentId,

View file

@ -3,15 +3,18 @@ package io.github.wulkanowy.sdk.scrapper.repository
import io.github.wulkanowy.sdk.scrapper.Scrapper
import io.github.wulkanowy.sdk.scrapper.exception.ScrapperException
import io.github.wulkanowy.sdk.scrapper.exception.StudentGraduateException
import io.github.wulkanowy.sdk.scrapper.getDecodedKey
import io.github.wulkanowy.sdk.scrapper.getNormalizedSymbol
import io.github.wulkanowy.sdk.scrapper.getScriptParam
import io.github.wulkanowy.sdk.scrapper.interceptor.handleErrors
import io.github.wulkanowy.sdk.scrapper.isCurrentLoginHasEduOne
import io.github.wulkanowy.sdk.scrapper.login.CertificateResponse
import io.github.wulkanowy.sdk.scrapper.login.InvalidSymbolException
import io.github.wulkanowy.sdk.scrapper.login.LoginHelper
import io.github.wulkanowy.sdk.scrapper.login.NotLoggedInException
import io.github.wulkanowy.sdk.scrapper.login.UrlGenerator
import io.github.wulkanowy.sdk.scrapper.register.AuthInfo
import io.github.wulkanowy.sdk.scrapper.register.Diary
import io.github.wulkanowy.sdk.scrapper.register.HomePageResponse
import io.github.wulkanowy.sdk.scrapper.register.PermissionUnit
import io.github.wulkanowy.sdk.scrapper.register.Permissions
@ -25,9 +28,9 @@ import io.github.wulkanowy.sdk.scrapper.repository.AccountRepository.Companion.S
import io.github.wulkanowy.sdk.scrapper.repository.AccountRepository.Companion.SELECTOR_ADFS_LIGHT
import io.github.wulkanowy.sdk.scrapper.repository.AccountRepository.Companion.SELECTOR_STANDARD
import io.github.wulkanowy.sdk.scrapper.service.RegisterService
import io.github.wulkanowy.sdk.scrapper.service.StudentPlusService
import io.github.wulkanowy.sdk.scrapper.service.StudentService
import io.github.wulkanowy.sdk.scrapper.service.SymbolService
import io.github.wulkanowy.sdk.scrapper.timetable.CacheResponse
import kotlinx.serialization.json.Json
import org.jsoup.Jsoup
import org.jsoup.nodes.Element
@ -48,6 +51,7 @@ internal class RegisterRepository(
private val loginHelper: LoginHelper,
private val register: RegisterService,
private val student: StudentService,
private val studentPlus: StudentPlusService,
private val symbolService: SymbolService,
private val url: UrlGenerator,
) {
@ -109,11 +113,14 @@ internal class RegisterRepository(
val errors = checkForErrors(symbolLoginType, homeResponse.getOrNull()?.document)
val studentModuleUrls = homeResponse.getOrNull()?.studentSchools.orEmpty()
.map { it.attr("href") }
val userName = homeResponse.getOrNull().getUserNameFromUserData()
val schools = homeResponse.getOrNull()
.toPermissions()
.toUnitsMap()
.getRegisterUnits(userName)
.getRegisterUnits(userName, studentModuleUrls)
loginHelper.logout()
@ -147,15 +154,19 @@ internal class RegisterRepository(
}
}
private suspend fun Map<PermissionUnit, AuthInfo?>.getRegisterUnits(userName: String): List<RegisterUnit> {
private suspend fun Map<PermissionUnit, AuthInfo?>.getRegisterUnits(userName: String, studentModuleUrls: List<String>): List<RegisterUnit> {
return map { (unit, authInfo) ->
url.schoolId = unit.symbol
val isEduOne = isCurrentLoginHasEduOne(studentModuleUrls, url)
val cacheAndDiaries = runCatching {
if (authInfo?.parentIds.isNullOrEmpty() && authInfo?.studentIds.isNullOrEmpty()) {
null to emptyList()
} else {
getStudentCache() to getStudentDiaries()
when {
isEduOne -> null to getEduOneDiaries()
else -> getStudentCache() to getStudentDiaries()
}
}
}
@ -261,8 +272,7 @@ internal class RegisterRepository(
}
// used only for check is student from parent account
// todo: handle eduOne case
private suspend fun getStudentCache(): CacheResponse? {
private suspend fun getStudentCache(): Boolean? {
val studentPageUrl = url.generate(UrlGenerator.Site.STUDENT) + "LoginEndpoint.aspx"
val start = student.getStart(studentPageUrl)
@ -283,16 +293,97 @@ internal class RegisterRepository(
else -> start
}
return student.getUserCache(
val userCache = student.getUserCache(
url = url.generate(UrlGenerator.Site.STUDENT) + "UczenCache.mvc/Get",
token = getScriptParam("antiForgeryToken", startPage),
appGuid = getScriptParam("appGuid", startPage),
appVersion = getScriptParam("version", startPage),
).data
return userCache?.isParent
}
private suspend fun getStudentDiaries() = student
private suspend fun getStudentDiaries(): List<Diary> = student
.getSchoolInfo(url.generate(UrlGenerator.Site.STUDENT) + "UczenDziennik.mvc/Get")
.handleErrors()
.data.orEmpty()
private suspend fun getEduOneDiaries(): List<Diary> {
val studentPageUrl = url.generate(UrlGenerator.Site.STUDENT_PLUS) + "LoginEndpoint.aspx"
val start = student.getStart(studentPageUrl)
if ("Working" in Jsoup.parse(start).title()) {
val cert = certificateAdapter.fromHtml(start)
student.sendModuleCertificate(
referer = url.createReferer(UrlGenerator.Site.STUDENT_PLUS),
url = cert.action,
certificate = mapOf(
"wa" to cert.wa,
"wresult" to cert.wresult,
"wctx" to cert.wctx,
),
)
}
return studentPlus
.getContext().students
.map { diary ->
val key = getDecodedKey(diary.key)
val semesters = studentPlus.getSemesters(
key = diary.key,
diaryId = diary.registerId,
).map { semester ->
Diary.Semester(
number = semester.numerOkresu,
start = semester.dataOd,
end = semester.dataDo,
unitId = key.unitId,
id = semester.id,
// todo
isLast = false,
level = 0,
classId = 0,
)
}
Diary(
id = diary.registerId,
studentId = key.studentId,
studentName = diary.studentName.substringBefore(" ", ""),
studentSecondName = diary.studentName.substringAfter(" ", "").substringBefore(" ", ""),
studentSurname = diary.studentName.substringAfterLast(" ", ""),
studentNick = "",
isDiary = true,
diaryId = key.diaryId,
kindergartenDiaryId = key.diaryId,
fosterDiaryId = key.diaryId,
level = diary.className.takeWhile { it.isDigit() }.toInt(), // todo
symbol = diary.className.takeWhile { it.isLetter() }, // todo
name = diary.className,
year = diary.registerDateFrom.year,
semesters = semesters,
start = diary.registerDateFrom,
end = diary.registerDateTo,
componentUnitId = key.unitId,
sioTypeId = null,
isAdults = diary.isAdults,
isPostSecondary = diary.isPolicealna,
is13 = diary.is13,
isArtistic = diary.isArtystyczna,
isArtistic13 = diary.isArtystyczna13,
isSpecial = diary.isSpecjalna,
isKindergarten = diary.isPrzedszkolak,
isFoster = null,
isArchived = null,
isCharges = diary.config.isPlatnosci,
isPayments = diary.config.isOplaty,
isPayButtonOn = null,
canMergeAccounts = diary.config.isScalanieKont,
fullName = diary.studentName,
o365PassType = null,
isAdult = null,
isAuthorized = !diary.isAuthorizationRequired,
citizenship = null,
)
}
}
}

View file

@ -10,6 +10,7 @@ import io.github.wulkanowy.sdk.scrapper.attendance.AttendanceExcusesPlusResponse
import io.github.wulkanowy.sdk.scrapper.attendance.SentExcuseStatus
import io.github.wulkanowy.sdk.scrapper.exception.FeatureDisabledException
import io.github.wulkanowy.sdk.scrapper.exception.VulcanClientError
import io.github.wulkanowy.sdk.scrapper.getEncodedKey
import io.github.wulkanowy.sdk.scrapper.handleErrors
import io.github.wulkanowy.sdk.scrapper.mobile.TokenResponse
import io.github.wulkanowy.sdk.scrapper.register.AuthorizePermissionPlusRequest
@ -21,8 +22,6 @@ import io.github.wulkanowy.sdk.scrapper.toFormat
import org.jsoup.Jsoup
import java.net.HttpURLConnection
import java.time.LocalDate
import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi
internal class StudentPlusRepository(
private val api: StudentPlusService,
@ -141,9 +140,4 @@ internal class StudentPlusRepository(
.split("data:image/png;base64,")[1],
)
}
@OptIn(ExperimentalEncodingApi::class)
private fun getEncodedKey(studentId: Int, diaryId: Int, unitId: Int): String {
return Base64.encode("$studentId-$diaryId-1-$unitId".toByteArray())
}
}

View file

@ -96,7 +96,7 @@ internal class StudentRepository(
suspend fun getStudent(studentId: Int, unitId: Int): RegisterStudent? {
return getStudentsFromDiaries(
cache = getCache(),
isParent = getCache().isParent,
diaries = api.getDiaries().handleErrors().data.orEmpty(),
unitId = unitId,
).find {

View file

@ -4,6 +4,7 @@ import io.github.wulkanowy.sdk.scrapper.attendance.Attendance
import io.github.wulkanowy.sdk.scrapper.attendance.AttendanceExcusePlusRequest
import io.github.wulkanowy.sdk.scrapper.attendance.AttendanceExcusesPlusResponse
import io.github.wulkanowy.sdk.scrapper.conferences.Conference
import io.github.wulkanowy.sdk.scrapper.grades.GradeSemester
import io.github.wulkanowy.sdk.scrapper.mobile.Device
import io.github.wulkanowy.sdk.scrapper.mobile.TokenResponse
import io.github.wulkanowy.sdk.scrapper.register.AuthorizePermissionPlusRequest
@ -20,6 +21,12 @@ internal interface StudentPlusService {
@GET("api/Context")
suspend fun getContext(): ContextResponse
@GET("api/OkresyKlasyfikacyjne")
suspend fun getSemesters(
@Query("key") key: String,
@Query("idDziennik") diaryId: Int,
): List<GradeSemester>
@POST("api/AutoryzacjaPesel")
suspend fun authorize(@Body body: AuthorizePermissionPlusRequest): Response<Unit>

View file

@ -9,6 +9,7 @@ import io.github.wulkanowy.sdk.scrapper.login.UrlGenerator
import io.github.wulkanowy.sdk.scrapper.repository.RegisterRepository
import io.github.wulkanowy.sdk.scrapper.service.LoginService
import io.github.wulkanowy.sdk.scrapper.service.RegisterService
import io.github.wulkanowy.sdk.scrapper.service.StudentPlusService
import io.github.wulkanowy.sdk.scrapper.service.StudentService
import io.github.wulkanowy.sdk.scrapper.service.SymbolService
import kotlinx.coroutines.test.runTest
@ -56,6 +57,7 @@ class RegisterTest : BaseLocalTest() {
),
),
student = getService(StudentService::class.java, "http://fakelog.localhost:3000", false),
studentPlus = getService(StudentPlusService::class.java, "http://fakelog.localhost:3000", false),
symbolService = getService(
service = SymbolService::class.java,
url = "http://fakelog.localhost:3000",

View file

@ -12,6 +12,7 @@ import io.github.wulkanowy.sdk.scrapper.register.RegisterStudent
import io.github.wulkanowy.sdk.scrapper.register.RegisterTest
import io.github.wulkanowy.sdk.scrapper.service.LoginService
import io.github.wulkanowy.sdk.scrapper.service.RegisterService
import io.github.wulkanowy.sdk.scrapper.service.StudentPlusService
import io.github.wulkanowy.sdk.scrapper.service.StudentService
import io.github.wulkanowy.sdk.scrapper.service.SymbolService
import kotlinx.coroutines.test.runTest
@ -45,6 +46,7 @@ class RegisterRepositoryTest : BaseLocalTest() {
okHttp = getOkHttp(errorInterceptor = false, autoLoginInterceptorOn = false),
),
student = getService(service = StudentService::class.java, html = false),
studentPlus = getService(service = StudentPlusService::class.java, html = false),
symbolService = getService(
service = SymbolService::class.java,
url = "http://fakelog.localhost:3000",