Try to fix attendance when eduOne is on

This commit is contained in:
Mikołaj Pich 2024-01-14 13:52:23 +01:00
parent 0bba02045f
commit cd68beb772
11 changed files with 95 additions and 56 deletions

View file

@ -16,6 +16,7 @@ import io.github.wulkanowy.sdk.scrapper.home.GovernmentUnit
import io.github.wulkanowy.sdk.scrapper.home.LuckyNumber
import io.github.wulkanowy.sdk.scrapper.homework.Homework
import io.github.wulkanowy.sdk.scrapper.login.LoginHelper
import io.github.wulkanowy.sdk.scrapper.login.UrlGenerator
import io.github.wulkanowy.sdk.scrapper.menu.Menu
import io.github.wulkanowy.sdk.scrapper.messages.Folder
import io.github.wulkanowy.sdk.scrapper.messages.Mailbox
@ -33,6 +34,7 @@ import io.github.wulkanowy.sdk.scrapper.repository.AccountRepository
import io.github.wulkanowy.sdk.scrapper.repository.HomepageRepository
import io.github.wulkanowy.sdk.scrapper.repository.MessagesRepository
import io.github.wulkanowy.sdk.scrapper.repository.RegisterRepository
import io.github.wulkanowy.sdk.scrapper.repository.StudentPlusRepository
import io.github.wulkanowy.sdk.scrapper.repository.StudentRepository
import io.github.wulkanowy.sdk.scrapper.repository.StudentStartRepository
import io.github.wulkanowy.sdk.scrapper.school.School
@ -65,6 +67,8 @@ class Scrapper {
private val cookieJarCabinet = CookieJarCabinet()
private var isEduOne = false
var logLevel: HttpLoggingInterceptor.Level = HttpLoggingInterceptor.Level.BASIC
set(value) {
if (field != value) changeManager.reset()
@ -237,6 +241,9 @@ class Scrapper {
buildTag = buildTag,
emptyCookieJarIntercept = emptyCookieJarInterceptor,
userAgentTemplate = userAgentTemplate,
onUserLoggedIn = { studentModuleUrls ->
isEduOne = isCurrentLoginHasEduOne(studentModuleUrls)
},
).apply {
appInterceptors.forEach { (interceptor, isNetwork) ->
setInterceptor(interceptor, isNetwork)
@ -244,6 +251,15 @@ class Scrapper {
}
}
private fun isCurrentLoginHasEduOne(studentModuleUrls: List<String>): Boolean {
return studentModuleUrls.any {
it.startsWith(
prefix = serviceManager.urlGenerator.generate(UrlGenerator.Site.STUDENT_PLUS),
ignoreCase = true,
)
}
}
private val account by lazy { AccountRepository(serviceManager.getAccountService()) }
private val register by resettableLazy(changeManager) {
@ -281,7 +297,12 @@ class Scrapper {
private val student: StudentRepository by resettableLazy(changeManager) {
StudentRepository(
api = serviceManager.getStudentService(),
studentPlusService = serviceManager.getStudentPlusService(),
)
}
private val studentPlus: StudentPlusRepository by resettableLazy(changeManager) {
StudentPlusRepository(
api = serviceManager.getStudentPlusService(),
)
}
@ -316,7 +337,10 @@ class Scrapper {
suspend fun getAttendance(startDate: LocalDate, endDate: LocalDate? = null): List<Attendance> {
if (diaryId == 0) return emptyList()
return student.getAttendance(startDate, endDate, studentId, diaryId)
return when (isEduOne) {
true -> studentPlus.getAttendance(startDate, endDate, studentId, diaryId)
else -> student.getAttendance(startDate, endDate)
}
}
suspend fun getAttendanceSummary(subjectId: Int? = -1): List<AttendanceSummary> {
@ -396,7 +420,10 @@ class Scrapper {
suspend fun getCompletedLessons(startDate: LocalDate, endDate: LocalDate? = null, subjectId: Int = -1): List<CompletedLesson> {
if (diaryId == 0) return emptyList()
return student.getCompletedLessons(startDate, endDate, subjectId)
return when (isEduOne) {
true -> studentPlus.getCompletedLessons()
else -> student.getCompletedLessons(startDate, endDate, subjectId)
}
}
suspend fun getRegisteredDevices(): List<Device> = student.getRegisteredDevices()

View file

@ -48,6 +48,7 @@ internal class AutoLoginInterceptor(
private val notLoggedInCallback: suspend () -> HomePageResponse,
private val fetchStudentCookies: () -> Pair<HttpUrl, Document>,
private val fetchMessagesCookies: () -> Pair<HttpUrl, Document>,
private val onUserLoggedIn: (studentModuleUrls: List<String>) -> Unit = {},
) : Interceptor {
companion object {
@ -84,8 +85,7 @@ internal class AutoLoginInterceptor(
try {
val homePageResponse = runBlocking { notLoggedInCallback() }
val studentModuleUrls = homePageResponse.studentSchools.map { it.attr("href") }
logger.debug("Found student module urls: {}", studentModuleUrls)
onUserLoggedIn(studentModuleUrls)
studentModuleHeaders = null
messagesModuleHeaders = null

View file

@ -0,0 +1,44 @@
package io.github.wulkanowy.sdk.scrapper.repository
import io.github.wulkanowy.sdk.scrapper.attendance.Attendance
import io.github.wulkanowy.sdk.scrapper.attendance.AttendanceCategory
import io.github.wulkanowy.sdk.scrapper.exception.FeatureDisabledException
import io.github.wulkanowy.sdk.scrapper.service.StudentPlusService
import io.github.wulkanowy.sdk.scrapper.timetable.CacheEduOneResponse
import io.github.wulkanowy.sdk.scrapper.timetable.CompletedLesson
import io.github.wulkanowy.sdk.scrapper.toFormat
import java.time.LocalDate
import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi
internal class StudentPlusRepository(
private val api: StudentPlusService,
) {
private fun LocalDate.toISOFormat(): String = toFormat("yyyy-MM-dd'T00:00:00'")
private suspend fun getCache(): CacheEduOneResponse {
return api.getUserCache()
}
suspend fun getAttendance(startDate: LocalDate, endDate: LocalDate?, studentId: Int, diaryId: Int): List<Attendance> {
return api.getAttendance(
key = getEncodedKey(studentId, diaryId),
from = startDate.toISOFormat(),
to = endDate?.toISOFormat() ?: startDate.plusDays(7).toISOFormat(),
).onEach {
it.category = AttendanceCategory.getCategoryById(it.categoryId)
}
}
suspend fun getCompletedLessons(): List<CompletedLesson> {
val cache = getCache()
if (!cache.showCompletedLessons) throw FeatureDisabledException("Widok lekcji zrealizowanych został wyłączony przez Administratora szkoły")
TODO("Not yet implemented")
}
@OptIn(ExperimentalEncodingApi::class)
private fun getEncodedKey(studentId: Int, diaryId: Int): String {
return Base64.encode("$studentId-$diaryId-1".toByteArray())
}
}

View file

@ -2,7 +2,6 @@ package io.github.wulkanowy.sdk.scrapper.repository
import io.github.wulkanowy.sdk.scrapper.attendance.Absent
import io.github.wulkanowy.sdk.scrapper.attendance.Attendance
import io.github.wulkanowy.sdk.scrapper.attendance.AttendanceCategory
import io.github.wulkanowy.sdk.scrapper.attendance.AttendanceExcuseRequest
import io.github.wulkanowy.sdk.scrapper.attendance.AttendanceRequest
import io.github.wulkanowy.sdk.scrapper.attendance.AttendanceSummary
@ -17,7 +16,6 @@ import io.github.wulkanowy.sdk.scrapper.exams.ExamRequest
import io.github.wulkanowy.sdk.scrapper.exams.mapExamsList
import io.github.wulkanowy.sdk.scrapper.exception.FeatureDisabledException
import io.github.wulkanowy.sdk.scrapper.getSchoolYear
import io.github.wulkanowy.sdk.scrapper.getScriptFlag
import io.github.wulkanowy.sdk.scrapper.grades.GradePointsSummary
import io.github.wulkanowy.sdk.scrapper.grades.GradeRequest
import io.github.wulkanowy.sdk.scrapper.grades.Grades
@ -46,7 +44,6 @@ import io.github.wulkanowy.sdk.scrapper.school.School
import io.github.wulkanowy.sdk.scrapper.school.Teacher
import io.github.wulkanowy.sdk.scrapper.school.mapToSchool
import io.github.wulkanowy.sdk.scrapper.school.mapToTeachers
import io.github.wulkanowy.sdk.scrapper.service.StudentPlusService
import io.github.wulkanowy.sdk.scrapper.service.StudentService
import io.github.wulkanowy.sdk.scrapper.student.StudentInfo
import io.github.wulkanowy.sdk.scrapper.student.StudentPhoto
@ -62,31 +59,17 @@ import io.github.wulkanowy.sdk.scrapper.timetable.mapTimetableList
import io.github.wulkanowy.sdk.scrapper.toFormat
import org.jsoup.Jsoup
import java.time.LocalDate
import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi
internal class StudentRepository(
private val api: StudentService,
private val studentPlusService: StudentPlusService,
) {
private var isEduOne: Boolean = false
private fun LocalDate.toISOFormat(): String = toFormat("yyyy-MM-dd'T00:00:00'")
private suspend fun getCache(): CacheResponse {
if (isEduOne) error("Cache unavailable in eduOne compatibility mode")
val startPage = getStartPage()
isEduOne = getScriptFlag("isEduOne", startPage)
if (isEduOne) error("Unsupported eduOne detected!")
val res = api.getUserCache().handleErrors()
val data = requireNotNull(res.data) {
"Required value was null. $res"
return api.getUserCache().handleErrors().let {
requireNotNull(it.data)
}
return data
}
suspend fun authorizePermission(pesel: String): Boolean {
@ -105,18 +88,8 @@ internal class StudentRepository(
}
}
@OptIn(ExperimentalEncodingApi::class)
suspend fun getAttendance(startDate: LocalDate, endDate: LocalDate?, studentId: Int, diaryId: Int): List<Attendance> {
suspend fun getAttendance(startDate: LocalDate, endDate: LocalDate?): List<Attendance> {
val lessonTimes = runCatching { getCache().times }
if (lessonTimes.isFailure && isEduOne) {
return studentPlusService.getAttendance(
key = Base64.encode("$studentId-$diaryId-1".toByteArray()),
from = startDate.toISOFormat(),
to = endDate?.toISOFormat() ?: startDate.plusDays(7).toISOFormat(),
).onEach {
it.category = AttendanceCategory.getCategoryById(it.categoryId)
}
}
return api.getAttendance(AttendanceRequest(startDate.atStartOfDay()))
.handleErrors()
.data?.mapAttendanceList(startDate, endDate, lessonTimes.getOrThrow()).orEmpty()
@ -279,8 +252,4 @@ internal class StudentRepository(
UnregisterDeviceRequest(id),
).handleErrors().success
}
private suspend fun getStartPage(): String {
return api.getStart()
}
}

View file

@ -49,6 +49,7 @@ internal class ServiceManager(
private val diaryId: Int,
private val kindergartenDiaryId: Int,
private val schoolYear: Int,
onUserLoggedIn: (studentModuleUrls: List<String>) -> Unit = {},
emptyCookieJarIntercept: Boolean,
androidVersion: String,
buildTag: String,
@ -101,6 +102,7 @@ internal class ServiceManager(
notLoggedInCallback = { loginHelper.login(email, password) },
fetchStudentCookies = { loginHelper.loginStudent() },
fetchMessagesCookies = { loginHelper.loginMessages() },
onUserLoggedIn = onUserLoggedIn,
) to false,
UserAgentInterceptor(androidVersion, buildTag, userAgentTemplate) to false,
HttpErrorInterceptor() to false,

View file

@ -3,18 +3,14 @@ package io.github.wulkanowy.sdk.scrapper.service
import io.github.wulkanowy.sdk.scrapper.attendance.Attendance
import io.github.wulkanowy.sdk.scrapper.conferences.Conference
import io.github.wulkanowy.sdk.scrapper.mobile.Device
import io.github.wulkanowy.sdk.scrapper.timetable.CacheResponse
import io.github.wulkanowy.sdk.scrapper.timetable.CacheEduOneResponse
import retrofit2.http.GET
import retrofit2.http.Query
import retrofit2.http.Url
internal interface StudentPlusService {
@GET
suspend fun getStart(@Url url: String): String
@GET("api/Cache")
suspend fun getUserCache(): CacheResponse
suspend fun getUserCache(): CacheEduOneResponse
@GET("api/Frekwencja")
suspend fun getAttendance(

View file

@ -47,9 +47,6 @@ import retrofit2.http.Url
internal interface StudentService {
@GET("LoginEndpoint.aspx")
suspend fun getStart(): String
@GET
suspend fun getStart(@Url url: String): String

View file

@ -0,0 +1,10 @@
package io.github.wulkanowy.sdk.scrapper.timetable
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
internal data class CacheEduOneResponse(
@SerialName("isPokazLekcjeZrealizowaneOn")
val showCompletedLessons: Boolean,
)

View file

@ -10,7 +10,6 @@ import io.github.wulkanowy.sdk.scrapper.login.UrlGenerator
import io.github.wulkanowy.sdk.scrapper.register.HomePageResponse
import io.github.wulkanowy.sdk.scrapper.repository.StudentRepository
import io.github.wulkanowy.sdk.scrapper.service.LoginService
import io.github.wulkanowy.sdk.scrapper.service.StudentPlusService
import io.github.wulkanowy.sdk.scrapper.service.StudentService
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json
@ -56,7 +55,6 @@ abstract class BaseLocalTest : BaseTest() {
val okHttp = getOkHttp(errorInterceptor = true, autoLoginInterceptorOn = true, loginType = loginType, autoLogin = autoLogin)
return StudentRepository(
api = getService(StudentService::class.java, server.url("/").toString(), false, okHttp),
studentPlusService = getService(StudentPlusService::class.java, server.url("/").toString(), false, okHttp),
)
}

View file

@ -16,11 +16,10 @@ class AttendanceTest : BaseLocalTest() {
private val student by lazy {
val repo = getStudentRepo {
it.enqueue("WitrynaUcznia.html", RegisterTest::class.java)
it.enqueue("UczenCache.json", RegisterTest::class.java)
it.enqueue("Frekwencja.json", AttendanceTest::class.java)
}
runBlocking { repo.getAttendance(getLocalDate(2018, 10, 1), null, 1, 1) }
runBlocking { repo.getAttendance(getLocalDate(2018, 10, 1), null) }
}
@Test
@ -158,13 +157,11 @@ class AttendanceTest : BaseLocalTest() {
@Test
fun getAttendance_requestDateFormat() = runTest {
val repo = getStudentRepo {
it.enqueue("WitrynaUcznia.html", RegisterTest::class.java)
it.enqueue("UczenCache.json", RegisterTest::class.java)
it.enqueue("Frekwencja.json", AttendanceTest::class.java)
}
repo.getAttendance(getLocalDate(2018, 10, 1), null, 1, 2)
repo.getAttendance(getLocalDate(2018, 10, 1), null)
server.takeRequest()
server.takeRequest()
val request = server.takeRequest()

View file

@ -24,7 +24,6 @@ class CompletedLessonsTest : BaseLocalTest() {
@Before
fun setUp() {
server.enqueue("WitrynaUcznia.html", RegisterTest::class.java)
server.enqueue("UczenCache.json", RegisterTest::class.java)
}