Add fallback to eduOne attendance if there is eduOne: true in student start page

This commit is contained in:
Mikołaj Pich 2023-05-29 00:03:49 +02:00
parent f9be659490
commit 773402ceb1
15 changed files with 137 additions and 27 deletions

View file

@ -134,15 +134,16 @@ public final class io/github/wulkanowy/sdk/scrapper/attendance/Absent {
public final class io/github/wulkanowy/sdk/scrapper/attendance/Attendance {
public static final field Companion Lio/github/wulkanowy/sdk/scrapper/attendance/Attendance$Companion;
public field category Lio/github/wulkanowy/sdk/scrapper/attendance/AttendanceCategory;
public synthetic fun <init> (IILjava/time/LocalDateTime;Ljava/lang/String;ILkotlinx/serialization/internal/SerializationConstructorMarker;)V
public fun <init> (ILjava/time/LocalDateTime;Ljava/lang/String;I)V
public synthetic fun <init> (ILjava/time/LocalDateTime;Ljava/lang/String;IILkotlin/jvm/internal/DefaultConstructorMarker;)V
public synthetic fun <init> (IIILjava/time/LocalDateTime;Ljava/lang/String;ILkotlinx/serialization/internal/SerializationConstructorMarker;)V
public fun <init> (IILjava/time/LocalDateTime;Ljava/lang/String;I)V
public synthetic fun <init> (IILjava/time/LocalDateTime;Ljava/lang/String;IILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()I
public final fun component2 ()Ljava/time/LocalDateTime;
public final fun component3 ()Ljava/lang/String;
public final fun component4 ()I
public final fun copy (ILjava/time/LocalDateTime;Ljava/lang/String;I)Lio/github/wulkanowy/sdk/scrapper/attendance/Attendance;
public static synthetic fun copy$default (Lio/github/wulkanowy/sdk/scrapper/attendance/Attendance;ILjava/time/LocalDateTime;Ljava/lang/String;IILjava/lang/Object;)Lio/github/wulkanowy/sdk/scrapper/attendance/Attendance;
public final fun component2 ()I
public final fun component3 ()Ljava/time/LocalDateTime;
public final fun component4 ()Ljava/lang/String;
public final fun component5 ()I
public final fun copy (IILjava/time/LocalDateTime;Ljava/lang/String;I)Lio/github/wulkanowy/sdk/scrapper/attendance/Attendance;
public static synthetic fun copy$default (Lio/github/wulkanowy/sdk/scrapper/attendance/Attendance;IILjava/time/LocalDateTime;Ljava/lang/String;IILjava/lang/Object;)Lio/github/wulkanowy/sdk/scrapper/attendance/Attendance;
public fun equals (Ljava/lang/Object;)Z
public final fun getCategory ()Lio/github/wulkanowy/sdk/scrapper/attendance/AttendanceCategory;
public final fun getCategoryId ()I
@ -156,7 +157,6 @@ public final class io/github/wulkanowy/sdk/scrapper/attendance/Attendance {
public final fun setCategory (Lio/github/wulkanowy/sdk/scrapper/attendance/AttendanceCategory;)V
public final fun setExcusable (Z)V
public final fun setExcuseStatus (Lio/github/wulkanowy/sdk/scrapper/attendance/SentExcuseStatus;)V
public final fun setNumber (I)V
public fun toString ()Ljava/lang/String;
public static final synthetic fun write$Self (Lio/github/wulkanowy/sdk/scrapper/attendance/Attendance;Lkotlinx/serialization/encoding/CompositeEncoder;Lkotlinx/serialization/descriptors/SerialDescriptor;)V
}

View file

@ -249,7 +249,10 @@ class Scrapper {
}
private val student by resettableLazy(changeManager) {
StudentRepository(serviceManager.getStudentService())
StudentRepository(
api = serviceManager.getStudentService(),
studentPlusService = serviceManager.getStudentPlusService(),
)
}
private val messages by resettableLazy(changeManager) {
@ -277,7 +280,7 @@ class Scrapper {
suspend fun getAttendance(startDate: LocalDate, endDate: LocalDate? = null): List<Attendance> {
if (diaryId == 0) return emptyList()
return student.getAttendance(startDate, endDate)
return student.getAttendance(startDate, endDate, studentId, diaryId)
}
suspend fun getAttendanceSummary(subjectId: Int? = -1): List<AttendanceSummary> {

View file

@ -3,7 +3,7 @@ package io.github.wulkanowy.sdk.scrapper
import io.github.wulkanowy.sdk.scrapper.messages.Mailbox
import io.github.wulkanowy.sdk.scrapper.messages.Recipient
import io.github.wulkanowy.sdk.scrapper.messages.RecipientType
import org.jsoup.Jsoup.parse
import org.jsoup.Jsoup
import java.text.Normalizer
import java.text.SimpleDateFormat
import java.time.Instant.ofEpochMilli
@ -52,7 +52,13 @@ internal fun String.getGradePointPercent(): String {
internal fun getScriptParam(name: String, content: String, fallback: String = ""): String {
return "$name: '(.)*'".toRegex().find(content).let { result ->
if (null !== result) parse(result.groupValues[0].substringAfter("'").substringBefore("'")).text() else fallback
if (null !== result) Jsoup.parse(result.groupValues[0].substringAfter("'").substringBefore("'")).text() else fallback
}
}
internal fun getScriptFlag(name: String, content: String, fallback: Boolean = false): Boolean {
return "$name: (false|true)".toRegex().find(content).let { result ->
if (null !== result) result.groupValues[1].toBoolean() else fallback
}
}

View file

@ -1,31 +1,38 @@
package io.github.wulkanowy.sdk.scrapper.attendance
import io.github.wulkanowy.sdk.scrapper.adapter.CustomDateAdapter
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import kotlinx.serialization.json.JsonNames
import java.time.LocalDateTime
@Serializable
@OptIn(ExperimentalSerializationApi::class)
data class Attendance(
@SerialName("numerLekcji")
val number: Int = 0,
@SerialName("IdPoraLekcji")
@JsonNames("idPoraLekcji")
val timeId: Int = 0,
@SerialName("Data")
@JsonNames("data")
@Serializable(with = CustomDateAdapter::class)
val date: LocalDateTime,
@SerialName("PrzedmiotNazwa")
@JsonNames("opisZajec")
val subject: String?,
@SerialName("IdKategoria")
@JsonNames("kategoriaFrekwencji")
val categoryId: Int = -1,
) {
@Transient
var number: Int = 0
@Transient
lateinit var category: AttendanceCategory

View file

@ -13,8 +13,9 @@ internal fun AttendanceResponse.mapAttendanceList(start: LocalDate, end: LocalDa
val endDate = end ?: start.plusDays(4)
return lessons.map {
val sentExcuse = sentExcuses.firstOrNull { excuse -> excuse.date == it.date && excuse.timeId == it.timeId }
it.apply {
number = times.single { time -> time.id == it.timeId }.number
it.copy(
number = times.single { time -> time.id == it.timeId }.number,
).apply {
category = AttendanceCategory.getCategoryById(categoryId)
excusable = excuseActive && (category == ABSENCE_UNEXCUSED || category == UNEXCUSED_LATENESS) && sentExcuse == null
if (sentExcuse != null) excuseStatus = SentExcuseStatus.getByValue(sentExcuse.status)

View file

@ -1,31 +1,41 @@
package io.github.wulkanowy.sdk.scrapper.conferences
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import kotlinx.serialization.json.JsonNames
import java.time.LocalDateTime
@Serializable
@OptIn(ExperimentalSerializationApi::class)
data class Conference(
@SerialName("Tytul")
@JsonNames("sala")
val place: String,
@SerialName("TematZebrania")
@JsonNames("opis")
val topic: String,
@SerialName("Agenda")
@JsonNames("opis") // todo
val agenda: String,
@SerialName("ObecniNaZebraniu")
@JsonNames("obecniNaZebraniu")
val presentOnConference: String,
@SerialName("ZebranieOnline")
@JsonNames("zebranieOnline")
val online: String?,
@SerialName("Id")
@JsonNames("id")
val id: Int,
@Transient
@JsonNames("dataCzas") // todo
val date: LocalDateTime = LocalDateTime.now(),
)

View file

@ -17,12 +17,17 @@ internal class UrlGenerator(
LOGIN,
HOME,
STUDENT,
STUDENT_PLUS,
MESSAGES,
;
val isStudent: Boolean
get() = this == STUDENT_PLUS || this == STUDENT
}
fun generate(type: Site): String {
if (type == Site.BASE) return "$schema://$host"
return "$schema://${getSubDomain(type)}$domainSuffix.$host/$symbol/${if (type == Site.STUDENT) "$schoolId/" else ""}"
return "$schema://${getSubDomain(type)}$domainSuffix.$host/$symbol/${if (type.isStudent) "$schoolId/" else ""}"
}
private fun getSubDomain(type: Site): String {
@ -30,6 +35,7 @@ internal class UrlGenerator(
Site.LOGIN -> "cufs"
Site.HOME -> "uonetplus"
Site.STUDENT -> "uonetplus-uczen"
Site.STUDENT_PLUS -> "uonetplus-uczenplus"
Site.MESSAGES -> "uonetplus-wiadomosciplus"
else -> error("unknown")
}

View file

@ -1,23 +1,29 @@
package io.github.wulkanowy.sdk.scrapper.mobile
import io.github.wulkanowy.sdk.scrapper.adapter.CustomDateAdapter
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonNames
import java.time.LocalDateTime
@Serializable
@OptIn(ExperimentalSerializationApi::class)
data class Device(
@SerialName("Id")
@JsonNames("id")
val id: Int = 0,
@SerialName("IdentyfikatorUrzadzenia")
val deviceId: String? = null,
@SerialName("NazwaUrzadzenia")
@JsonNames("nazwa")
val name: String? = null,
@SerialName("DataUtworzenia")
@JsonNames("dataCertyfikatu")
@Serializable(with = CustomDateAdapter::class)
val createDate: LocalDateTime? = null,

View file

@ -17,6 +17,7 @@ import io.github.wulkanowy.sdk.scrapper.exams.mapExamsList
import io.github.wulkanowy.sdk.scrapper.exception.FeatureDisabledException
import io.github.wulkanowy.sdk.scrapper.exception.ScrapperException
import io.github.wulkanowy.sdk.scrapper.getSchoolYear
import io.github.wulkanowy.sdk.scrapper.getScriptFlag
import io.github.wulkanowy.sdk.scrapper.getScriptParam
import io.github.wulkanowy.sdk.scrapper.grades.GradePointsSummary
import io.github.wulkanowy.sdk.scrapper.grades.GradeRequest
@ -46,6 +47,7 @@ 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,14 +64,25 @@ import io.github.wulkanowy.sdk.scrapper.toFormat
import org.jsoup.Jsoup
import java.net.HttpURLConnection.HTTP_NOT_FOUND
import java.time.LocalDate
import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi
internal class StudentRepository(private val api: StudentService) {
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(
token = getScriptParam("antiForgeryToken", startPage),
appGuid = getScriptParam("appGuid", startPage),
@ -102,11 +115,20 @@ internal class StudentRepository(private val api: StudentService) {
}
}
suspend fun getAttendance(startDate: LocalDate, endDate: LocalDate?): List<Attendance> {
val lessonTimes = getCache().times
@Suppress("UnnecessaryOptInAnnotation")
@OptIn(ExperimentalEncodingApi::class)
suspend fun getAttendance(startDate: LocalDate, endDate: LocalDate?, studentId: Int, diaryId: Int): 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(),
)
}
return api.getAttendance(AttendanceRequest(startDate.atStartOfDay()))
.handleErrors()
.data?.mapAttendanceList(startDate, endDate, lessonTimes).orEmpty()
.data?.mapAttendanceList(startDate, endDate, lessonTimes.getOrThrow()).orEmpty()
}
suspend fun getAttendanceSummary(subjectId: Int?): List<AttendanceSummary> {

View file

@ -154,6 +154,14 @@ internal class ServiceManager(
).create()
}
fun getStudentPlusService(withLogin: Boolean = true, studentInterceptor: Boolean = true): StudentPlusService {
return getRetrofit(
client = prepareStudentService(withLogin, studentInterceptor),
baseUrl = urlGenerator.generate(UrlGenerator.Site.STUDENT_PLUS),
json = true,
).create()
}
private fun prepareStudentService(withLogin: Boolean, studentInterceptor: Boolean): OkHttpClient.Builder {
if (withLogin && schoolId.isBlank()) throw ScrapperException("School id is not set")

View file

@ -0,0 +1,31 @@
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 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
@GET("api/Frekwencja")
suspend fun getAttendance(
@Query("key") key: String,
@Query("dataOd") from: String,
@Query("dataDo") to: String,
): List<Attendance>
@GET("api/ZarejestrowaneUrzadzenia")
suspend fun getRegisteredDevices(): List<Device>
@GET("api/Zebrania")
suspend fun getConferences(): List<Conference>
}

View file

@ -1,23 +1,29 @@
package io.github.wulkanowy.sdk.scrapper.timetable
import io.github.wulkanowy.sdk.scrapper.adapter.CustomDateAdapter
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonNames
import java.time.LocalDateTime
@Serializable
@OptIn(ExperimentalSerializationApi::class)
internal class CacheResponse {
@SerialName("isParentUser")
@JsonNames("isParent")
var isParent: Boolean = false
@SerialName("poryLekcji")
var times: List<Time> = emptyList()
@SerialName("isMenuOn")
@JsonNames("isMenu")
var isMenu: Boolean = false
@SerialName("pokazLekcjeZrealizowane")
@JsonNames("isPokazLekcjeZrealizowaneOn")
var showCompletedLessons: Boolean = false
@Serializable

View file

@ -8,6 +8,7 @@ import io.github.wulkanowy.sdk.scrapper.interceptor.HttpErrorInterceptor
import io.github.wulkanowy.sdk.scrapper.login.LoginHelper
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
@ -49,7 +50,10 @@ abstract class BaseLocalTest : BaseTest() {
internal fun getStudentRepo(loginType: Scrapper.LoginType = Scrapper.LoginType.STANDARD, autoLogin: Boolean = false, responses: (MockWebServer) -> Unit): StudentRepository {
responses(server)
val okHttp = getOkHttp(errorInterceptor = true, autoLoginInterceptorOn = true, loginType = loginType, autoLogin = autoLogin)
return StudentRepository(getService(StudentService::class.java, server.url("/").toString(), false, okHttp))
return StudentRepository(
api = getService(StudentService::class.java, server.url("/").toString(), false, okHttp),
studentPlusService = getService(StudentPlusService::class.java, server.url("/").toString(), false, okHttp),
)
}
@OptIn(ExperimentalSerializationApi::class)

View file

@ -128,7 +128,7 @@ class ScrapperRemoteTest : BaseTest() {
@Test
fun attendanceTest() {
val attendance = runBlocking { api.getAttendance(getLocalDate(2018, 10, 1)) }
val attendance = runBlocking { api.getAttendance(getLocalDate(2023, 5, 1), getLocalDate(2023, 5, 30)) }
attendance[0].run {
assertEquals(1, number)

View file

@ -20,7 +20,7 @@ class AttendanceTest : BaseLocalTest() {
it.enqueue("UczenCache.json", RegisterTest::class.java)
it.enqueue("Frekwencja.json", AttendanceTest::class.java)
}
runBlocking { repo.getAttendance(getLocalDate(2018, 10, 1), null) }
runBlocking { repo.getAttendance(getLocalDate(2018, 10, 1), null, 1, 1) }
}
@Test
@ -162,7 +162,7 @@ class AttendanceTest : BaseLocalTest() {
it.enqueue("UczenCache.json", RegisterTest::class.java)
it.enqueue("Frekwencja.json", AttendanceTest::class.java)
}
runBlocking { repo.getAttendance(getLocalDate(2018, 10, 1), null) }
runBlocking { repo.getAttendance(getLocalDate(2018, 10, 1), null, 1, 2) }
server.takeRequest()
server.takeRequest()