io.github.wulkanowy.sdk.scrapper.repository.StudentPlusRepository.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of sdk-scrapper Show documentation
Show all versions of sdk-scrapper Show documentation
Unified way of retrieving data from the UONET+ register through mobile api and scraping api
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.AttendanceExcusePlusRequest
import io.github.wulkanowy.sdk.scrapper.attendance.AttendanceExcusePlusRequestItem
import io.github.wulkanowy.sdk.scrapper.attendance.AttendanceExcusePlusResponseItem
import io.github.wulkanowy.sdk.scrapper.attendance.AttendanceExcusesPlusResponse
import io.github.wulkanowy.sdk.scrapper.attendance.AttendanceSummary
import io.github.wulkanowy.sdk.scrapper.attendance.SentExcuseStatus
import io.github.wulkanowy.sdk.scrapper.conferences.Conference
import io.github.wulkanowy.sdk.scrapper.exams.Exam
import io.github.wulkanowy.sdk.scrapper.exception.FeatureDisabledException
import io.github.wulkanowy.sdk.scrapper.exception.VulcanClientError
import io.github.wulkanowy.sdk.scrapper.getDecodedKey
import io.github.wulkanowy.sdk.scrapper.getEncodedKey
import io.github.wulkanowy.sdk.scrapper.grades.Grades
import io.github.wulkanowy.sdk.scrapper.grades.mapGradesList
import io.github.wulkanowy.sdk.scrapper.grades.mapGradesSummary
import io.github.wulkanowy.sdk.scrapper.handleErrors
import io.github.wulkanowy.sdk.scrapper.homework.Homework
import io.github.wulkanowy.sdk.scrapper.mobile.Device
import io.github.wulkanowy.sdk.scrapper.mobile.TokenResponse
import io.github.wulkanowy.sdk.scrapper.notes.Note
import io.github.wulkanowy.sdk.scrapper.notes.NoteCategory
import io.github.wulkanowy.sdk.scrapper.register.AuthorizePermissionPlusRequest
import io.github.wulkanowy.sdk.scrapper.register.ContextStudent
import io.github.wulkanowy.sdk.scrapper.register.RegisterStudent
import io.github.wulkanowy.sdk.scrapper.register.Semester
import io.github.wulkanowy.sdk.scrapper.register.mapToSemester
import io.github.wulkanowy.sdk.scrapper.school.School
import io.github.wulkanowy.sdk.scrapper.school.Teacher
import io.github.wulkanowy.sdk.scrapper.service.StudentPlusService
import io.github.wulkanowy.sdk.scrapper.student.StudentInfo
import io.github.wulkanowy.sdk.scrapper.student.StudentPhoto
import io.github.wulkanowy.sdk.scrapper.timetable.CompletedLesson
import io.github.wulkanowy.sdk.scrapper.timetable.Lesson
import io.github.wulkanowy.sdk.scrapper.timetable.Timetable
import io.github.wulkanowy.sdk.scrapper.timetable.TimetableDayHeader
import io.github.wulkanowy.sdk.scrapper.timetable.mapCompletedLessons
import io.github.wulkanowy.sdk.scrapper.toFormat
import org.jsoup.Jsoup
import org.slf4j.LoggerFactory
import java.net.HttpURLConnection
import java.time.LocalDate
import java.time.Month
internal class StudentPlusRepository(
private val api: StudentPlusService,
) {
private companion object {
@JvmStatic
private val logger = LoggerFactory.getLogger(this::class.java)
const val REPLACEMENT = 1
const val RELOCATION = 2
const val CANCELLATION = 3
}
private fun LocalDate.toISOFormat(): String = toFormat("yyyy-MM-dd'T00:00:00'")
suspend fun authorizePermission(pesel: String, studentId: Int, diaryId: Int, unitId: Int): Boolean {
runCatching {
api.authorize(
body = AuthorizePermissionPlusRequest(
key = getKey(studentId, diaryId, unitId),
pesel = pesel,
),
)
}.onFailure {
if (it is VulcanClientError && it.httpCode == HttpURLConnection.HTTP_BAD_REQUEST) {
if ("odrzucona" in it.message.orEmpty()) {
return false
}
}
}.getOrThrow()
return true
}
suspend fun getStudent(studentId: Int): RegisterStudent {
val contextStudent = getMatchingStudent(studentId, null, null)
?: throw NoSuchElementException()
val semesters = runCatching {
when {
contextStudent.isAuthorizationRequired -> emptyList()
else -> api.getSemesters(
key = contextStudent.key,
diaryId = contextStudent.registerId,
)
}
}.onFailure {
logger.error("Can't fetch semesters", it)
}.getOrNull().orEmpty()
return RegisterStudent(
studentId = studentId,
studentName = contextStudent.studentName.substringBefore(" "),
studentSurname = contextStudent.studentName.substringAfterLast(" "),
className = contextStudent.className,
isParent = contextStudent.opiekunUcznia,
semesters = semesters.mapToSemester(contextStudent),
isAuthorized = !contextStudent.isAuthorizationRequired,
isEduOne = true, // we already in eduOne context here
studentSecondName = "", //
classId = 0,
schoolName = "",
schoolNameShort = null,
unitId = getDecodedKey(contextStudent.key).unitId,
)
}
suspend fun getSemesters(studentId: Int): List {
val student = getMatchingStudent(studentId, null, null)
?: throw NoSuchElementException()
return api.getSemesters(
key = student.key,
diaryId = student.registerId,
).mapToSemester(student)
}
suspend fun getAttendance(startDate: LocalDate, endDate: LocalDate?, studentId: Int, diaryId: Int, unitId: Int): List {
val key = getKey(studentId, diaryId, unitId)
val from = startDate.toISOFormat()
val to = endDate?.toISOFormat() ?: startDate.plusDays(7).toISOFormat()
val attendanceItems = api.getAttendance(key = key, from = from, to = to)
val sentExcuses = api.getExcuses(key = key, from = from, to = to)
return attendanceItems.onEach {
it.category = AttendanceCategory.getCategoryById(it.categoryId)
val sentExcuse = it.getMatchingExcuse(sentExcuses)
it.excusable = it.isExcusable(sentExcuses.isExcusesActive, sentExcuse)
if (sentExcuse != null) it.excuseStatus = SentExcuseStatus.getByValue(sentExcuse.status)
}
}
suspend fun getAttendanceSummary(studentId: Int, diaryId: Int, unitId: Int): List {
val summaries = api.getAttendanceSummary(key = getKey(studentId, diaryId, unitId))
val stats = summaries.items.associate { it.id to it.months }
val getMonthValue = fun(type: Int, month: Int): Int {
return stats[type]?.find { it.month == month }?.value ?: 0
}
return (1..12).map {
AttendanceSummary(
month = Month.of(it),
presence = getMonthValue(AttendanceCategory.PRESENCE.id, it),
absence = getMonthValue(AttendanceCategory.ABSENCE_UNEXCUSED.id, it),
absenceExcused = getMonthValue(AttendanceCategory.ABSENCE_EXCUSED.id, it),
absenceForSchoolReasons = getMonthValue(AttendanceCategory.ABSENCE_FOR_SCHOOL_REASONS.id, it),
lateness = getMonthValue(AttendanceCategory.UNEXCUSED_LATENESS.id, it),
latenessExcused = getMonthValue(AttendanceCategory.EXCUSED_LATENESS.id, it),
exemption = getMonthValue(AttendanceCategory.EXEMPTION.id, it),
)
}.filterNot { summary ->
summary.absence == 0 &&
summary.absenceExcused == 0 &&
summary.absenceForSchoolReasons == 0 &&
summary.exemption == 0 &&
summary.lateness == 0 &&
summary.latenessExcused == 0 &&
summary.presence == 0
}
}
private fun Attendance.getMatchingExcuse(sentExcuses: AttendanceExcusesPlusResponse): AttendanceExcusePlusResponseItem? {
return sentExcuses.excuses.find { excuse ->
excuse.dayDate == date && (excuse.lessonNumber == number || excuse.lessonNumber == null)
}
}
private fun Attendance.isExcusable(isExcusesActive: Boolean, sentExcuse: AttendanceExcusePlusResponseItem?): Boolean {
val isUnexcused = category == AttendanceCategory.ABSENCE_UNEXCUSED
val isUnexcusedLateness = category == AttendanceCategory.UNEXCUSED_LATENESS
val isStatusMatch = isUnexcused || isUnexcusedLateness
return isStatusMatch && isExcusesActive && sentExcuse == null
}
suspend fun excuseForAbsence(absents: List, content: String?, studentId: Int, diaryId: Int, unitId: Int): Boolean {
api.excuseForAbsence(
body = AttendanceExcusePlusRequest(
key = getKey(studentId, diaryId, unitId),
content = content.orEmpty(),
excuses = absents.map {
AttendanceExcusePlusRequestItem(
date = it.date.toFormat("yyyy-MM-dd'T'HH:mm:ss"),
lessonHourId = it.timeId,
)
},
),
).handleErrors()
return true
}
suspend fun getCompletedLessons(startDate: LocalDate, endDate: LocalDate?, studentId: Int, diaryId: Int, unitId: Int): List {
val key = getKey(studentId, diaryId, unitId)
val studentConfig = getMatchingStudent(studentId, diaryId, unitId)?.config
if (studentConfig?.showCompletedLessons != true) {
throw FeatureDisabledException("Widok lekcji zrealizowanych został wyłączony przez Administratora szkoły")
}
return api.getCompletedLessons(
key = key,
status = 1,
from = startDate.toISOFormat(),
to = endDate?.toISOFormat() ?: startDate.plusDays(7).toISOFormat(),
).mapCompletedLessons(startDate, endDate)
}
suspend fun getRegisteredDevices(studentId: Int, diaryId: Int, unitId: Int): List {
val key = getKey(studentId, diaryId, unitId)
return api.getRegisteredDevices(key = key)
}
suspend fun getToken(studentId: Int, diaryId: Int, unitId: Int): TokenResponse {
val key = getKey(studentId, diaryId, unitId)
api.createDeviceRegistrationToken(body = mapOf("key" to key))
val res = api.getDeviceRegistrationToken(key = key)
return res.copy(
qrCodeImage = Jsoup.parse(res.qrCodeImage)
.select("img")
.attr("src")
.split("data:image/png;base64,")[1],
)
}
suspend fun getGrades(semesterId: Int, studentId: Int, diaryId: Int, unitId: Int): Grades {
val key = getKey(studentId, diaryId, unitId)
val res = api.getGrades(
key = key,
semesterId = semesterId,
)
return Grades(
details = res.mapGradesList(),
summary = res.mapGradesSummary(),
descriptive = res.gradesDescriptive,
isAverage = res.isAverage,
isPoints = res.isPoints,
isForAdults = res.isForAdults,
type = res.type,
)
}
suspend fun getExams(startDate: LocalDate, endDate: LocalDate?, studentId: Int, diaryId: Int, unitId: Int): List {
val key = getKey(studentId, diaryId, unitId)
val examsHomeworkRes = api.getExamsAndHomework(
key = key,
from = startDate.toISOFormat(),
to = endDate?.toISOFormat(),
)
return examsHomeworkRes.filter { it.type != 4 }.map { exam ->
val examDetailsRes = api.getExamDetails(key = key, id = exam.id)
Exam(
entryDate = exam.date,
subject = exam.subject,
type = exam.type,
description = examDetailsRes.description,
teacher = examDetailsRes.teacher,
).apply {
typeName = when (exam.type) {
1 -> "Sprawdzian"
2 -> "Kartkówka"
else -> "Praca klasowa"
}
date = exam.date
teacherSymbol = ""
}
}
}
suspend fun getHomework(startDate: LocalDate, endDate: LocalDate?, studentId: Int, diaryId: Int, unitId: Int): List {
val key = getKey(studentId, diaryId, unitId)
val examsHomeworkRes = api.getExamsAndHomework(
key = key,
from = startDate.toISOFormat(),
to = endDate?.toISOFormat(),
)
return examsHomeworkRes.filter { it.type == 4 }.map { homework ->
val homeworkDetailsRes = api.getHomeworkDetails(key = key, id = homework.id)
Homework(
homeworkId = homework.id,
subject = homework.subject,
teacher = homeworkDetailsRes.teacher,
content = homeworkDetailsRes.description,
date = homework.date,
entryDate = homework.date,
status = homeworkDetailsRes.status,
isAnswerRequired = homeworkDetailsRes.isAnswerRequired,
).apply {
teacherSymbol = ""
}
}
}
suspend fun getTimetable(startDate: LocalDate, endDate: LocalDate?, studentId: Int, diaryId: Int, unitId: Int): Timetable {
val key = getKey(studentId, diaryId, unitId)
val defaultEndDate = (endDate ?: startDate.plusDays(7))
val lessons = api.getTimetable(
key = key,
from = startDate.toISOFormat(),
to = defaultEndDate?.toISOFormat(),
).map { lesson ->
Lesson(
number = 0,
start = lesson.godzinaOd,
end = lesson.godzinaDo,
date = lesson.godzinaOd.toLocalDate(),
subject = lesson.przedmiot,
subjectOld = lesson.zmiany.find { !it.zajecia.isNullOrBlank() }?.zajecia.orEmpty(),
group = lesson.podzial.orEmpty(),
room = lesson.sala,
roomOld = lesson.zmiany.find { !it.sala.isNullOrBlank() }?.sala.orEmpty(),
teacher = lesson.prowadzacy,
teacherOld = lesson.zmiany.find { !it.prowadzacy.isNullOrBlank() }?.prowadzacy.orEmpty(),
info = buildString {
lesson.zmiany.forEach {
when (it.typProwadzacego) {
0 -> {
if (it.zmiana != 6) {
append("Oddział nieobecny. ")
}
}
1 -> {
if (it.zmiana != 6) {
append("Nieobecny nauczyciel. ")
}
}
}
when (it.zmiana) {
1 -> append("Skutek nieobecności: ${it.informacjeNieobecnosc}")
2 -> append(it.informacjeNieobecnosc) // todo
3 -> append(it.informacjeNieobecnosc) // todo
4 -> append("Powód nieobecności: ${it.informacjeNieobecnosc}")
// przeniesienie z dnia
5 -> {
append("Zajęcia są przeniesione na: ")
append(it.dzien?.toLocalDate())
append(" w godzinach ")
append(it.godzinaOd?.toLocalTime())
append("-")
append(it.godzinaDo?.toLocalTime())
}
// przeniesienie na dzień
6 -> {
append("Zajęcia są przeniesione z dnia ")
append(it.dzien?.toLocalDate())
append(" w godzinach ")
append(it.godzinaOd?.toLocalTime())
append("-")
append(it.godzinaDo?.toLocalTime())
}
// zastępstwo nauczyciela
7 -> append("Zaplanowane jest zastępstwo za nauczyciela: ${it.prowadzacy}")
}
append(". ")
}
}.trim().trim('.'),
changes = lesson.adnotacja == REPLACEMENT || lesson.adnotacja == RELOCATION || lesson.zmiany.isNotEmpty(),
canceled = lesson.adnotacja == CANCELLATION || lesson.zmiany.any { it.zmiana == 1 || it.zmiana == 4 || it.zmiana == 5 },
)
}.sortedBy { it.start }
val headers = api.getTimetableFreeDays(
key = key,
from = startDate.toISOFormat(),
to = endDate?.toISOFormat(),
)
val days = (startDate.toEpochDay()..defaultEndDate.toEpochDay()).map(LocalDate::ofEpochDay)
val processedHeaders = days.map { processedDate ->
val exactMatch = headers.find {
it.dataOd.toLocalDate() == processedDate && it.dataDo.toLocalDate() == processedDate
}
val rangeMatch = headers.find {
processedDate in it.dataOd.toLocalDate()..it.dataDo.toLocalDate()
}
TimetableDayHeader(
date = processedDate,
content = (exactMatch ?: rangeMatch)?.nazwa.orEmpty(),
)
}
return Timetable(
lessons = lessons,
headers = processedHeaders,
// todo
additional = emptyList(),
)
}
suspend fun getNotes(studentId: Int, diaryId: Int, unitId: Int): List {
val key = getKey(studentId, diaryId, unitId)
return api.getNotes(key = key)
.map {
it.copy(
category = it.category.orEmpty(),
categoryType = NoteCategory.UNKNOWN.id,
).apply {
teacherSymbol = ""
}
}
.sortedWith(compareBy({ it.date }, { it.category }))
}
suspend fun getConferences(studentId: Int, diaryId: Int, unitId: Int): List {
val key = getKey(studentId, diaryId, unitId)
return api.getConferences(key = key)
}
suspend fun getTeachers(studentId: Int, diaryId: Int, unitId: Int): List {
val key = getKey(studentId, diaryId, unitId)
return api.getTeachers(key = key).teachers.map {
Teacher(
name = "${it.firstName} ${it.lastName}".trim(),
subject = it.subject,
)
}
}
suspend fun getSchool(studentId: Int, diaryId: Int, unitId: Int): School {
val key = getKey(studentId, diaryId, unitId)
return api.getSchool(key = key).let {
val streetNumber = it.buildingNumber + it.apartmentNumber.takeIf(String::isNotEmpty)?.let { "/$it" }.orEmpty()
val name = buildString {
append(it.name)
if (it.number.isNotEmpty()) {
append(" nr ${it.number}")
}
if (it.patron.isNotEmpty()) {
append(" im. ${it.patron}")
}
if (it.town.isNotEmpty()) {
append(" w ${it.town}")
}
}
School(
name = name,
address = "${it.street} $streetNumber, ${it.postcode} ${it.town}",
contact = it.workPhone,
headmaster = it.headmasters?.firstOrNull().orEmpty(),
pedagogue = "",
)
}
}
suspend fun getStudentInfo(studentId: Int, diaryId: Int, unitId: Int): StudentInfo {
val key = getKey(studentId, diaryId, unitId)
val studentInfo = api.getStudentInfo(key = key)
return studentInfo.copy(
birthDate = studentInfo.birthDateEduOne?.atStartOfDay(),
guardianFirst = studentInfo.guardianFirst?.let {
it.copy(
fullName = "${it.name} ${it.lastName}",
)
},
)
}
suspend fun getStudentPhoto(studentId: Int, diaryId: Int, unitId: Int): StudentPhoto {
val key = getKey(studentId, diaryId, unitId)
return api.getStudentPhoto(key = key) ?: StudentPhoto(photoBase64 = null)
}
private suspend fun getMatchingStudent(studentId: Int, diaryId: Int?, unitId: Int?): ContextStudent? {
val students = api.getContext().students
val exactMatch = students.find {
if (diaryId != null && unitId != null) {
it.key == getEncodedKey(studentId, diaryId, unitId)
} else {
getDecodedKey(it.key).studentId == studentId
}
}
if (exactMatch != null) return exactMatch
// todo: match based on student name?
return when (students.size) {
0 -> null
1 -> students.single()
else -> error("VULCAN okropnie utrudnił Wulkanowemu dopasowanie ucznia zapisanego na urządzeniu z tym na stronie i dlatego dane w apce nie chcą się załadować")
}
}
private suspend fun getKey(studentId: Int, diaryId: Int, unitId: Int): String {
return requireNotNull(getMatchingStudent(studentId, diaryId, unitId)?.key)
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy