From e75d8fc7921dc83e59730a1e5a92a7963067a541 Mon Sep 17 00:00:00 2001 From: Stevek Date: Tue, 3 Mar 2026 17:16:54 +0100 Subject: [PATCH 01/13] Add Certificate data class --- .../io/github/tomhula/jecnaapi/data/cert/Certificate.kt | 7 +++++++ .../io/github/tomhula/jecnaapi/data/schoolStaff/Teacher.kt | 6 +++++- 2 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 src/commonMain/kotlin/io/github/tomhula/jecnaapi/data/cert/Certificate.kt diff --git a/src/commonMain/kotlin/io/github/tomhula/jecnaapi/data/cert/Certificate.kt b/src/commonMain/kotlin/io/github/tomhula/jecnaapi/data/cert/Certificate.kt new file mode 100644 index 00000000..6f2a7cb6 --- /dev/null +++ b/src/commonMain/kotlin/io/github/tomhula/jecnaapi/data/cert/Certificate.kt @@ -0,0 +1,7 @@ +package io.github.tomhula.jecnaapi.data.cert + +data class Certificate( + val dateIssued: String, + val issuer: String, + val label: String, +) diff --git a/src/commonMain/kotlin/io/github/tomhula/jecnaapi/data/schoolStaff/Teacher.kt b/src/commonMain/kotlin/io/github/tomhula/jecnaapi/data/schoolStaff/Teacher.kt index f136d72f..5dbf79f8 100644 --- a/src/commonMain/kotlin/io/github/tomhula/jecnaapi/data/schoolStaff/Teacher.kt +++ b/src/commonMain/kotlin/io/github/tomhula/jecnaapi/data/schoolStaff/Teacher.kt @@ -1,6 +1,7 @@ package io.github.tomhula.jecnaapi.data.schoolStaff import io.github.tomhula.jecnaapi.data.SchoolAttendee +import io.github.tomhula.jecnaapi.data.cert.Certificate import io.github.tomhula.jecnaapi.data.timetable.Timetable class Teacher( @@ -16,7 +17,8 @@ class Teacher( val cabinet: String? = null, val tutorOfClass: String? = null, val consultationHours: String? = null, - val timetable: Timetable? = null + val timetable: Timetable? = null, + val certificates: List = emptyList(), ) : SchoolAttendee(fullName, username, schoolMail, privateMail, phoneNumbers, profilePicturePath) { val tag = tag.trim().lowercase().replaceFirstChar { it.uppercaseChar() } @@ -34,6 +36,7 @@ class Teacher( if (tutorOfClass != other.tutorOfClass) return false if (consultationHours != other.consultationHours) return false if (tag != other.tag) return false + if (certificates != other.certificates) return false return true } @@ -64,6 +67,7 @@ class Teacher( "tutorOfClass=$tutorOfClass, " + "consultationHours=$consultationHours, " + "tag='$tag'" + + "certificates=$certificates" + ")" } } From 6e3b0f1ff3671f56851e46a57321f2792d671c74 Mon Sep 17 00:00:00 2001 From: Stevek Date: Tue, 3 Mar 2026 19:17:57 +0100 Subject: [PATCH 02/13] Add certificate parser --- .../jecnaapi/parser/parsers/TeacherParser.kt | 26 +++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/src/commonMain/kotlin/io/github/tomhula/jecnaapi/parser/parsers/TeacherParser.kt b/src/commonMain/kotlin/io/github/tomhula/jecnaapi/parser/parsers/TeacherParser.kt index 687d1c76..2b07d1f0 100644 --- a/src/commonMain/kotlin/io/github/tomhula/jecnaapi/parser/parsers/TeacherParser.kt +++ b/src/commonMain/kotlin/io/github/tomhula/jecnaapi/parser/parsers/TeacherParser.kt @@ -1,9 +1,10 @@ package io.github.tomhula.jecnaapi.parser.parsers -import io.github.tomhula.jecnaapi.data.schoolStaff.Teacher -import io.github.tomhula.jecnaapi.parser.ParseException import com.fleeksoft.ksoup.Ksoup import com.fleeksoft.ksoup.nodes.Element +import io.github.tomhula.jecnaapi.data.cert.Certificate +import io.github.tomhula.jecnaapi.data.schoolStaff.Teacher +import io.github.tomhula.jecnaapi.parser.ParseException /** https://www.spsejecna.cz/ucitel/{teacher-tag} */ internal class TeacherParser(private val timetableParser: TimetableParser) @@ -14,7 +15,8 @@ internal class TeacherParser(private val timetableParser: TimetableParser) { val document = Ksoup.parse(html) val table = document.selectFirstOrThrow(".userprofile", "data table") - + val certList = document.select("ul.certifications > li") + val fullName = getTableValue(table, "Jméno")!! val tag = getTableValue(table, "Zkratka")!! val username = getTableValue(table, "Uživatelské jméno")!! @@ -28,10 +30,23 @@ internal class TeacherParser(private val timetableParser: TimetableParser) val consultationHours = getTableValue(table, "Konzultační hodiny") val tutorOfClass = getTableValue(table, "Třídní učitel") val profilePicturePath = document.selectFirst(".profilephoto .image img")?.attr("src") - + val certificates = mutableListOf() + val timetableEle = document.selectFirst("table.timetable") val timetable = timetableEle?.let { timetableParser.parse(it.outerHtml()) } + for (li in certList) + { + val date = li.selectFirst("span.date")?.text()?.trim().orEmpty() + val infoSpan = li.selectFirst("span.info") + val label = infoSpan?.selectFirst("span.label")?.text()?.trim().orEmpty() + val institution = infoSpan?.selectFirst("span.institution")?.text()?.trim().orEmpty() + if (date.isNotEmpty() && label.isNotEmpty()) + { + certificates.add(Certificate(date, institution, label)) + } + } + return Teacher( fullName = fullName, username = username, @@ -45,7 +60,8 @@ internal class TeacherParser(private val timetableParser: TimetableParser) cabinet = cabinet, tutorOfClass = tutorOfClass, consultationHours = consultationHours, - timetable = timetable + timetable = timetable, + certificates = certificates ) } catch (e: Exception) From 98daddf2b16a2cc1b1cd80b37c9423aa83d8c1e2 Mon Sep 17 00:00:00 2001 From: Stevek Date: Tue, 3 Mar 2026 19:31:26 +0100 Subject: [PATCH 03/13] Add certificate parser for student --- .../github/tomhula/jecnaapi/WebJecnaClient.kt | 18 +++++++------- .../parser/parsers/CertificatePageParser.kt | 24 +++++++++++++++++++ 2 files changed, 32 insertions(+), 10 deletions(-) create mode 100644 src/commonMain/kotlin/io/github/tomhula/jecnaapi/parser/parsers/CertificatePageParser.kt diff --git a/src/commonMain/kotlin/io/github/tomhula/jecnaapi/WebJecnaClient.kt b/src/commonMain/kotlin/io/github/tomhula/jecnaapi/WebJecnaClient.kt index 3545d033..3633b7d3 100644 --- a/src/commonMain/kotlin/io/github/tomhula/jecnaapi/WebJecnaClient.kt +++ b/src/commonMain/kotlin/io/github/tomhula/jecnaapi/WebJecnaClient.kt @@ -2,8 +2,6 @@ package io.github.tomhula.jecnaapi import com.fleeksoft.ksoup.Ksoup import com.fleeksoft.ksoup.nodes.Document -import io.ktor.client.statement.* -import io.ktor.http.* import io.github.tomhula.jecnaapi.data.notification.NotificationReference import io.github.tomhula.jecnaapi.parser.parsers.* import io.github.tomhula.jecnaapi.util.JecnaPeriodEncoder @@ -12,14 +10,13 @@ import io.github.tomhula.jecnaapi.util.SchoolYear import io.github.tomhula.jecnaapi.util.SchoolYearHalf import io.github.tomhula.jecnaapi.web.Auth import io.github.tomhula.jecnaapi.web.AuthenticationException -import io.ktor.client.HttpClient -import io.ktor.client.plugins.HttpTimeout -import io.ktor.client.plugins.cookies.AcceptAllCookiesStorage -import io.ktor.client.plugins.cookies.HttpCookies -import io.ktor.client.plugins.cookies.addCookie -import io.ktor.client.plugins.defaultRequest -import io.ktor.client.request.forms.submitForm -import io.ktor.client.request.get +import io.ktor.client.* +import io.ktor.client.plugins.* +import io.ktor.client.plugins.cookies.* +import io.ktor.client.request.* +import io.ktor.client.request.forms.* +import io.ktor.client.statement.* +import io.ktor.http.* import kotlinx.datetime.Month import kotlin.time.Clock import kotlin.time.Duration @@ -272,6 +269,7 @@ class WebJecnaClient( const val STUDENT = "/student" const val LOCKER = "/locker/student" const val ROOMS = "/ucebna" + const val CERTIFICATES = "/certification/student" } } diff --git a/src/commonMain/kotlin/io/github/tomhula/jecnaapi/parser/parsers/CertificatePageParser.kt b/src/commonMain/kotlin/io/github/tomhula/jecnaapi/parser/parsers/CertificatePageParser.kt new file mode 100644 index 00000000..7f3c8a07 --- /dev/null +++ b/src/commonMain/kotlin/io/github/tomhula/jecnaapi/parser/parsers/CertificatePageParser.kt @@ -0,0 +1,24 @@ +package io.github.tomhula.jecnaapi.parser.parsers + +import com.fleeksoft.ksoup.Ksoup +import io.github.tomhula.jecnaapi.data.cert.Certificate + +object CertificatePageParser +{ + fun parse(html: String): List + { + val document = Ksoup.parse(html) + val lis = document.select("ul.list li") + val certificates = lis.map { li -> + val labelText = li.select("span.label").text() + val parts = labelText.split(" / ", limit = 2) + val issuer = parts[0] + val rest = parts[1] + val dateParts = rest.split(" ze dne ", limit = 2) + val title = dateParts[0] + val dateIssued = dateParts[1] + Certificate(dateIssued, issuer, title) + } + return certificates + } +} From 2988a41c2249e278318e717d345a5dc91f2019b7 Mon Sep 17 00:00:00 2001 From: Stevek Date: Tue, 3 Mar 2026 20:06:45 +0100 Subject: [PATCH 04/13] Fix imports --- src/commonMain/kotlin/io/github/tomhula/jecnaapi/JecnaClient.kt | 2 ++ .../kotlin/io/github/tomhula/jecnaapi/WebJecnaClient.kt | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/commonMain/kotlin/io/github/tomhula/jecnaapi/JecnaClient.kt b/src/commonMain/kotlin/io/github/tomhula/jecnaapi/JecnaClient.kt index 047a49b5..7ddc6785 100644 --- a/src/commonMain/kotlin/io/github/tomhula/jecnaapi/JecnaClient.kt +++ b/src/commonMain/kotlin/io/github/tomhula/jecnaapi/JecnaClient.kt @@ -2,6 +2,7 @@ package io.github.tomhula.jecnaapi import io.github.tomhula.jecnaapi.data.absence.AbsencesPage import io.github.tomhula.jecnaapi.data.article.NewsPage import io.github.tomhula.jecnaapi.data.attendance.AttendancesPage +import io.github.tomhula.jecnaapi.data.cert.Certificate import io.github.tomhula.jecnaapi.data.grade.GradesPage import io.github.tomhula.jecnaapi.data.notification.Notification import io.github.tomhula.jecnaapi.data.notification.NotificationReference @@ -48,6 +49,7 @@ interface JecnaClient suspend fun getStudentProfile(username: String): Student suspend fun getNotifications(): List suspend fun getNotification(notification: NotificationReference): Notification + suspend fun getStudentCertificates(): List? companion object { diff --git a/src/commonMain/kotlin/io/github/tomhula/jecnaapi/WebJecnaClient.kt b/src/commonMain/kotlin/io/github/tomhula/jecnaapi/WebJecnaClient.kt index 3633b7d3..05c0fdd0 100644 --- a/src/commonMain/kotlin/io/github/tomhula/jecnaapi/WebJecnaClient.kt +++ b/src/commonMain/kotlin/io/github/tomhula/jecnaapi/WebJecnaClient.kt @@ -80,6 +80,7 @@ class WebJecnaClient( private val lockerPageParser = LockerPageParser private val roomsPageParser = RoomsPageParser private val roomParser = RoomParser(TimetableParser) + private val certificateParser = CertificatePageParser @OptIn(ExperimentalTime::class) override suspend fun login(auth: Auth): Boolean @@ -158,6 +159,7 @@ class WebJecnaClient( override suspend fun getStudentProfile() = autoLoginAuth?.let { getStudentProfile(it.username)} ?: throw AuthenticationException() override suspend fun getNotification(notification: NotificationReference) = notificationParser.getNotification(queryStringBody("${PageWebPath.NOTIFICATION}?userStudentRecordId=${notification.recordId}")) override suspend fun getNotifications() = notificationParser.parse(queryStringBody(PageWebPath.NOTIFICATIONS)) + override suspend fun getStudentCertificates() = certificateParser.parse(queryStringBody(PageWebPath.CERTIFICATES)) suspend fun setRole(role: Role) { From ff3b054b72b2840832d8453ef701d8fae1d2a372 Mon Sep 17 00:00:00 2001 From: Stevek Date: Tue, 3 Mar 2026 20:07:13 +0100 Subject: [PATCH 05/13] Fix imports --- .../io/github/tomhula/jecnaapi/WebJecnaClient.kt | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/commonMain/kotlin/io/github/tomhula/jecnaapi/WebJecnaClient.kt b/src/commonMain/kotlin/io/github/tomhula/jecnaapi/WebJecnaClient.kt index 05c0fdd0..f073bb4e 100644 --- a/src/commonMain/kotlin/io/github/tomhula/jecnaapi/WebJecnaClient.kt +++ b/src/commonMain/kotlin/io/github/tomhula/jecnaapi/WebJecnaClient.kt @@ -10,13 +10,16 @@ import io.github.tomhula.jecnaapi.util.SchoolYear import io.github.tomhula.jecnaapi.util.SchoolYearHalf import io.github.tomhula.jecnaapi.web.Auth import io.github.tomhula.jecnaapi.web.AuthenticationException -import io.ktor.client.* -import io.ktor.client.plugins.* -import io.ktor.client.plugins.cookies.* -import io.ktor.client.request.* -import io.ktor.client.request.forms.* import io.ktor.client.statement.* import io.ktor.http.* +import io.ktor.client.HttpClient +import io.ktor.client.plugins.HttpTimeout +import io.ktor.client.plugins.cookies.AcceptAllCookiesStorage +import io.ktor.client.plugins.cookies.HttpCookies +import io.ktor.client.plugins.cookies.addCookie +import io.ktor.client.plugins.defaultRequest +import io.ktor.client.request.forms.submitForm +import io.ktor.client.request.get import kotlinx.datetime.Month import kotlin.time.Clock import kotlin.time.Duration From 7d367a14baa19775c84f85899b6b08f24816a46e Mon Sep 17 00:00:00 2001 From: Stevek Date: Wed, 4 Mar 2026 20:58:20 +0100 Subject: [PATCH 06/13] Change to LocalDate --- .../io/github/tomhula/jecnaapi/data/cert/Certificate.kt | 4 +++- .../jecnaapi/parser/parsers/CertificatePageParser.kt | 3 ++- .../tomhula/jecnaapi/parser/parsers/TeacherParser.kt | 8 ++++++-- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/commonMain/kotlin/io/github/tomhula/jecnaapi/data/cert/Certificate.kt b/src/commonMain/kotlin/io/github/tomhula/jecnaapi/data/cert/Certificate.kt index 6f2a7cb6..1902fb57 100644 --- a/src/commonMain/kotlin/io/github/tomhula/jecnaapi/data/cert/Certificate.kt +++ b/src/commonMain/kotlin/io/github/tomhula/jecnaapi/data/cert/Certificate.kt @@ -1,7 +1,9 @@ package io.github.tomhula.jecnaapi.data.cert +import kotlinx.datetime.LocalDate + data class Certificate( - val dateIssued: String, + val dateIssued: LocalDate, val issuer: String, val label: String, ) diff --git a/src/commonMain/kotlin/io/github/tomhula/jecnaapi/parser/parsers/CertificatePageParser.kt b/src/commonMain/kotlin/io/github/tomhula/jecnaapi/parser/parsers/CertificatePageParser.kt index 7f3c8a07..e5a15f96 100644 --- a/src/commonMain/kotlin/io/github/tomhula/jecnaapi/parser/parsers/CertificatePageParser.kt +++ b/src/commonMain/kotlin/io/github/tomhula/jecnaapi/parser/parsers/CertificatePageParser.kt @@ -2,6 +2,7 @@ package io.github.tomhula.jecnaapi.parser.parsers import com.fleeksoft.ksoup.Ksoup import io.github.tomhula.jecnaapi.data.cert.Certificate +import kotlinx.datetime.LocalDate object CertificatePageParser { @@ -16,7 +17,7 @@ object CertificatePageParser val rest = parts[1] val dateParts = rest.split(" ze dne ", limit = 2) val title = dateParts[0] - val dateIssued = dateParts[1] + val dateIssued = LocalDate.parse(dateParts[1], CommonParser.CZECH_DATE_FORMAT_WITH_PADDING) Certificate(dateIssued, issuer, title) } return certificates diff --git a/src/commonMain/kotlin/io/github/tomhula/jecnaapi/parser/parsers/TeacherParser.kt b/src/commonMain/kotlin/io/github/tomhula/jecnaapi/parser/parsers/TeacherParser.kt index 2b07d1f0..812f8db2 100644 --- a/src/commonMain/kotlin/io/github/tomhula/jecnaapi/parser/parsers/TeacherParser.kt +++ b/src/commonMain/kotlin/io/github/tomhula/jecnaapi/parser/parsers/TeacherParser.kt @@ -5,6 +5,7 @@ import com.fleeksoft.ksoup.nodes.Element import io.github.tomhula.jecnaapi.data.cert.Certificate import io.github.tomhula.jecnaapi.data.schoolStaff.Teacher import io.github.tomhula.jecnaapi.parser.ParseException +import kotlinx.datetime.LocalDate /** https://www.spsejecna.cz/ucitel/{teacher-tag} */ internal class TeacherParser(private val timetableParser: TimetableParser) @@ -37,11 +38,14 @@ internal class TeacherParser(private val timetableParser: TimetableParser) for (li in certList) { - val date = li.selectFirst("span.date")?.text()?.trim().orEmpty() + val date = LocalDate.parse( + li.selectFirst("span.date")?.text()?.trim().orEmpty(), + CommonParser.CZECH_DATE_FORMAT_WITH_PADDING + ) val infoSpan = li.selectFirst("span.info") val label = infoSpan?.selectFirst("span.label")?.text()?.trim().orEmpty() val institution = infoSpan?.selectFirst("span.institution")?.text()?.trim().orEmpty() - if (date.isNotEmpty() && label.isNotEmpty()) + if (date.toString().isNotEmpty() && label.isNotEmpty()) { certificates.add(Certificate(date, institution, label)) } From 4cf26906db8e5725048295a8e4df2a1812326f47 Mon Sep 17 00:00:00 2001 From: Tomas Hula Date: Fri, 13 Mar 2026 11:46:15 +0100 Subject: [PATCH 07/13] Rename JecnaWebClient.certifcateParser to certificatePageParser --- .../kotlin/io/github/tomhula/jecnaapi/WebJecnaClient.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/commonMain/kotlin/io/github/tomhula/jecnaapi/WebJecnaClient.kt b/src/commonMain/kotlin/io/github/tomhula/jecnaapi/WebJecnaClient.kt index f073bb4e..df9e37f3 100644 --- a/src/commonMain/kotlin/io/github/tomhula/jecnaapi/WebJecnaClient.kt +++ b/src/commonMain/kotlin/io/github/tomhula/jecnaapi/WebJecnaClient.kt @@ -83,7 +83,7 @@ class WebJecnaClient( private val lockerPageParser = LockerPageParser private val roomsPageParser = RoomsPageParser private val roomParser = RoomParser(TimetableParser) - private val certificateParser = CertificatePageParser + private val certificatePageParser = CertificatePageParser @OptIn(ExperimentalTime::class) override suspend fun login(auth: Auth): Boolean @@ -162,7 +162,7 @@ class WebJecnaClient( override suspend fun getStudentProfile() = autoLoginAuth?.let { getStudentProfile(it.username)} ?: throw AuthenticationException() override suspend fun getNotification(notification: NotificationReference) = notificationParser.getNotification(queryStringBody("${PageWebPath.NOTIFICATION}?userStudentRecordId=${notification.recordId}")) override suspend fun getNotifications() = notificationParser.parse(queryStringBody(PageWebPath.NOTIFICATIONS)) - override suspend fun getStudentCertificates() = certificateParser.parse(queryStringBody(PageWebPath.CERTIFICATES)) + override suspend fun getStudentCertificates() = certificatePageParser.parse(queryStringBody(PageWebPath.CERTIFICATES)) suspend fun setRole(role: Role) { From eebe4c2ec69939eb1978bcc1a9fe9f2a06f7dd94 Mon Sep 17 00:00:00 2001 From: Stevek Date: Fri, 13 Mar 2026 14:18:49 +0100 Subject: [PATCH 08/13] Imporvements --- .../tomhula/jecnaapi/parser/parsers/TeacherParser.kt | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/commonMain/kotlin/io/github/tomhula/jecnaapi/parser/parsers/TeacherParser.kt b/src/commonMain/kotlin/io/github/tomhula/jecnaapi/parser/parsers/TeacherParser.kt index 812f8db2..159167ad 100644 --- a/src/commonMain/kotlin/io/github/tomhula/jecnaapi/parser/parsers/TeacherParser.kt +++ b/src/commonMain/kotlin/io/github/tomhula/jecnaapi/parser/parsers/TeacherParser.kt @@ -16,8 +16,6 @@ internal class TeacherParser(private val timetableParser: TimetableParser) { val document = Ksoup.parse(html) val table = document.selectFirstOrThrow(".userprofile", "data table") - val certList = document.select("ul.certifications > li") - val fullName = getTableValue(table, "Jméno")!! val tag = getTableValue(table, "Zkratka")!! val username = getTableValue(table, "Uživatelské jméno")!! @@ -31,11 +29,11 @@ internal class TeacherParser(private val timetableParser: TimetableParser) val consultationHours = getTableValue(table, "Konzultační hodiny") val tutorOfClass = getTableValue(table, "Třídní učitel") val profilePicturePath = document.selectFirst(".profilephoto .image img")?.attr("src") - val certificates = mutableListOf() - val timetableEle = document.selectFirst("table.timetable") val timetable = timetableEle?.let { timetableParser.parse(it.outerHtml()) } + val certificates = mutableListOf() + val certList = document.select("ul.certifications > li") for (li in certList) { val date = LocalDate.parse( @@ -45,7 +43,7 @@ internal class TeacherParser(private val timetableParser: TimetableParser) val infoSpan = li.selectFirst("span.info") val label = infoSpan?.selectFirst("span.label")?.text()?.trim().orEmpty() val institution = infoSpan?.selectFirst("span.institution")?.text()?.trim().orEmpty() - if (date.toString().isNotEmpty() && label.isNotEmpty()) + if (institution.isNotEmpty()) { certificates.add(Certificate(date, institution, label)) } From 48075240e8fb81e96af7c53ba243757c1633a016 Mon Sep 17 00:00:00 2001 From: Stevek Date: Fri, 13 Mar 2026 14:31:10 +0100 Subject: [PATCH 09/13] Add access denied handling --- .../kotlin/io/github/tomhula/jecnaapi/WebJecnaClient.kt | 9 +++++++-- .../jecnaapi/parser/parsers/CertificatePageParser.kt | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/commonMain/kotlin/io/github/tomhula/jecnaapi/WebJecnaClient.kt b/src/commonMain/kotlin/io/github/tomhula/jecnaapi/WebJecnaClient.kt index df9e37f3..fb2b9d39 100644 --- a/src/commonMain/kotlin/io/github/tomhula/jecnaapi/WebJecnaClient.kt +++ b/src/commonMain/kotlin/io/github/tomhula/jecnaapi/WebJecnaClient.kt @@ -211,14 +211,18 @@ class WebJecnaClient( * A query with autologin handling. * * @throws AuthenticationException If the request fails because of authentication. (even after autologin) + * @return null if redirected to /neopravneny-pristup (access denied) */ - suspend fun query(path: String, parameters: Parameters?): HttpResponse + suspend fun query(path: String, parameters: Parameters?): HttpResponse? { val response = plainQuery(path, parameters) /* No redirect to login. */ val locationHeader = response.headers[HttpHeaders.Location] ?: return response.also { autoLoginAttempted = false } + if (locationHeader == "$endpoint/neopravneny-pristup") + return null + if (!locationHeader.startsWith("$endpoint/user/need-login")) return response.also { autoLoginAttempted = false } @@ -239,7 +243,8 @@ class WebJecnaClient( return query(path, parameters) } - suspend fun queryStringBody(path: String, parameters: Parameters? = null) = query(path, parameters).bodyAsText() + suspend fun queryStringBody(path: String, parameters: Parameters? = null) = + query(path, parameters)?.bodyAsText() ?: throw IllegalStateException("Access denied to resource") /** Closes the HTTP client. */ fun close() = httpClient.close() diff --git a/src/commonMain/kotlin/io/github/tomhula/jecnaapi/parser/parsers/CertificatePageParser.kt b/src/commonMain/kotlin/io/github/tomhula/jecnaapi/parser/parsers/CertificatePageParser.kt index e5a15f96..7d373bd4 100644 --- a/src/commonMain/kotlin/io/github/tomhula/jecnaapi/parser/parsers/CertificatePageParser.kt +++ b/src/commonMain/kotlin/io/github/tomhula/jecnaapi/parser/parsers/CertificatePageParser.kt @@ -11,7 +11,7 @@ object CertificatePageParser val document = Ksoup.parse(html) val lis = document.select("ul.list li") val certificates = lis.map { li -> - val labelText = li.select("span.label").text() + val labelText = li.select("span.label").text().trim() val parts = labelText.split(" / ", limit = 2) val issuer = parts[0] val rest = parts[1] From 878aa41e155a9df7f58bbaa185653779c5928ac0 Mon Sep 17 00:00:00 2001 From: Stevek Date: Fri, 13 Mar 2026 14:31:57 +0100 Subject: [PATCH 10/13] remove nullable --- src/commonMain/kotlin/io/github/tomhula/jecnaapi/JecnaClient.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commonMain/kotlin/io/github/tomhula/jecnaapi/JecnaClient.kt b/src/commonMain/kotlin/io/github/tomhula/jecnaapi/JecnaClient.kt index 7ddc6785..950b7fb6 100644 --- a/src/commonMain/kotlin/io/github/tomhula/jecnaapi/JecnaClient.kt +++ b/src/commonMain/kotlin/io/github/tomhula/jecnaapi/JecnaClient.kt @@ -49,7 +49,7 @@ interface JecnaClient suspend fun getStudentProfile(username: String): Student suspend fun getNotifications(): List suspend fun getNotification(notification: NotificationReference): Notification - suspend fun getStudentCertificates(): List? + suspend fun getStudentCertificates(): List companion object { From 071290aab75785657de70527b21ea1a0707a55a6 Mon Sep 17 00:00:00 2001 From: Tomas Hula Date: Thu, 19 Mar 2026 15:46:04 +0100 Subject: [PATCH 11/13] Revert "Add access denied handling" This reverts commit 48075240e8fb81e96af7c53ba243757c1633a016. --- .../kotlin/io/github/tomhula/jecnaapi/WebJecnaClient.kt | 9 ++------- .../jecnaapi/parser/parsers/CertificatePageParser.kt | 2 +- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/src/commonMain/kotlin/io/github/tomhula/jecnaapi/WebJecnaClient.kt b/src/commonMain/kotlin/io/github/tomhula/jecnaapi/WebJecnaClient.kt index fb2b9d39..df9e37f3 100644 --- a/src/commonMain/kotlin/io/github/tomhula/jecnaapi/WebJecnaClient.kt +++ b/src/commonMain/kotlin/io/github/tomhula/jecnaapi/WebJecnaClient.kt @@ -211,18 +211,14 @@ class WebJecnaClient( * A query with autologin handling. * * @throws AuthenticationException If the request fails because of authentication. (even after autologin) - * @return null if redirected to /neopravneny-pristup (access denied) */ - suspend fun query(path: String, parameters: Parameters?): HttpResponse? + suspend fun query(path: String, parameters: Parameters?): HttpResponse { val response = plainQuery(path, parameters) /* No redirect to login. */ val locationHeader = response.headers[HttpHeaders.Location] ?: return response.also { autoLoginAttempted = false } - if (locationHeader == "$endpoint/neopravneny-pristup") - return null - if (!locationHeader.startsWith("$endpoint/user/need-login")) return response.also { autoLoginAttempted = false } @@ -243,8 +239,7 @@ class WebJecnaClient( return query(path, parameters) } - suspend fun queryStringBody(path: String, parameters: Parameters? = null) = - query(path, parameters)?.bodyAsText() ?: throw IllegalStateException("Access denied to resource") + suspend fun queryStringBody(path: String, parameters: Parameters? = null) = query(path, parameters).bodyAsText() /** Closes the HTTP client. */ fun close() = httpClient.close() diff --git a/src/commonMain/kotlin/io/github/tomhula/jecnaapi/parser/parsers/CertificatePageParser.kt b/src/commonMain/kotlin/io/github/tomhula/jecnaapi/parser/parsers/CertificatePageParser.kt index 7d373bd4..e5a15f96 100644 --- a/src/commonMain/kotlin/io/github/tomhula/jecnaapi/parser/parsers/CertificatePageParser.kt +++ b/src/commonMain/kotlin/io/github/tomhula/jecnaapi/parser/parsers/CertificatePageParser.kt @@ -11,7 +11,7 @@ object CertificatePageParser val document = Ksoup.parse(html) val lis = document.select("ul.list li") val certificates = lis.map { li -> - val labelText = li.select("span.label").text().trim() + val labelText = li.select("span.label").text() val parts = labelText.split(" / ", limit = 2) val issuer = parts[0] val rest = parts[1] From ae8e7037040a46a37b50ff4f533575edae5261f1 Mon Sep 17 00:00:00 2001 From: Tomas Hula Date: Thu, 19 Mar 2026 15:53:46 +0100 Subject: [PATCH 12/13] Handle certifications for students not in 4th grade --- .../io/github/tomhula/jecnaapi/WebJecnaClient.kt | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/commonMain/kotlin/io/github/tomhula/jecnaapi/WebJecnaClient.kt b/src/commonMain/kotlin/io/github/tomhula/jecnaapi/WebJecnaClient.kt index df9e37f3..fbcc80cd 100644 --- a/src/commonMain/kotlin/io/github/tomhula/jecnaapi/WebJecnaClient.kt +++ b/src/commonMain/kotlin/io/github/tomhula/jecnaapi/WebJecnaClient.kt @@ -2,6 +2,7 @@ package io.github.tomhula.jecnaapi import com.fleeksoft.ksoup.Ksoup import com.fleeksoft.ksoup.nodes.Document +import io.github.tomhula.jecnaapi.data.cert.Certificate import io.github.tomhula.jecnaapi.data.notification.NotificationReference import io.github.tomhula.jecnaapi.parser.parsers.* import io.github.tomhula.jecnaapi.util.JecnaPeriodEncoder @@ -162,7 +163,16 @@ class WebJecnaClient( override suspend fun getStudentProfile() = autoLoginAuth?.let { getStudentProfile(it.username)} ?: throw AuthenticationException() override suspend fun getNotification(notification: NotificationReference) = notificationParser.getNotification(queryStringBody("${PageWebPath.NOTIFICATION}?userStudentRecordId=${notification.recordId}")) override suspend fun getNotifications() = notificationParser.parse(queryStringBody(PageWebPath.NOTIFICATIONS)) - override suspend fun getStudentCertificates() = certificatePageParser.parse(queryStringBody(PageWebPath.CERTIFICATES)) + override suspend fun getStudentCertificates(): List + { + val response = query(PageWebPath.CERTIFICATES, parameters = null) + val locationHeader = response.headers[HttpHeaders.Location] + + if (locationHeader == "$endpoint/neopravneny-pristup") + return emptyList() + + return certificatePageParser.parse(response.bodyAsText()) + } suspend fun setRole(role: Role) { From a649dfe494278b8cab4a2c81daface6c01b8caeb Mon Sep 17 00:00:00 2001 From: Tomas Hula Date: Thu, 19 Mar 2026 15:54:16 +0100 Subject: [PATCH 13/13] Trim certificate label --- .../tomhula/jecnaapi/parser/parsers/CertificatePageParser.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commonMain/kotlin/io/github/tomhula/jecnaapi/parser/parsers/CertificatePageParser.kt b/src/commonMain/kotlin/io/github/tomhula/jecnaapi/parser/parsers/CertificatePageParser.kt index e5a15f96..7d373bd4 100644 --- a/src/commonMain/kotlin/io/github/tomhula/jecnaapi/parser/parsers/CertificatePageParser.kt +++ b/src/commonMain/kotlin/io/github/tomhula/jecnaapi/parser/parsers/CertificatePageParser.kt @@ -11,7 +11,7 @@ object CertificatePageParser val document = Ksoup.parse(html) val lis = document.select("ul.list li") val certificates = lis.map { li -> - val labelText = li.select("span.label").text() + val labelText = li.select("span.label").text().trim() val parts = labelText.split(" / ", limit = 2) val issuer = parts[0] val rest = parts[1]