[API/Vulcan] Implement Vulcan lucky numbers.

This commit is contained in:
Kuba Szczodrzyński 2020-04-22 20:05:36 +02:00
parent e8dad29e5d
commit f685a4dceb
11 changed files with 197 additions and 46 deletions

View File

@ -168,6 +168,7 @@ const val ERROR_VULCAN_WEB_CERTIFICATE_EXPIRED = 349
const val ERROR_VULCAN_WEB_LOGGED_OUT = 350
const val ERROR_VULCAN_WEB_CERTIFICATE_POST_FAILED = 351
const val ERROR_VULCAN_WEB_GRADUATE_ACCOUNT = 352
const val ERROR_VULCAN_WEB_NO_SCHOOLS = 353
const val ERROR_LOGIN_IDZIENNIK_WEB_INVALID_LOGIN = 401
const val ERROR_LOGIN_IDZIENNIK_WEB_INVALID_SCHOOL_NAME = 402

View File

@ -140,7 +140,10 @@ object Regexes {
"""\(przeniesiona (z|na) lekcj[ię] ([0-9]+), (.+)\)""".toRegex()
}
val VULCAN_WEB_PERMISSIONS by lazy {
"""permissions: '([A-z0-9\/=+\-_]+?)'""".toRegex()
"""permissions: '([A-z0-9/=+\-_]+?)'""".toRegex()
}
val VULCAN_WEB_SYMBOL_VALIDATE by lazy {
"""[A-z0-9]+""".toRegex(IGNORE_CASE)
}

View File

@ -17,9 +17,10 @@ import pl.szczodrzynski.edziennik.values
class DataVulcan(app: App, profile: Profile?, loginStore: LoginStore) : Data(app, profile, loginStore) {
fun isWebMainLoginValid() = currentSemesterEndDate-30 > currentTimeUnix()
&& apiFingerprint[symbol].isNotNullNorEmpty()
&& apiPrivateKey[symbol].isNotNullNorEmpty()
fun isWebMainLoginValid() = webExpiryTime-30 > currentTimeUnix()
&& webAuthCookie.isNotNullNorEmpty()
&& webHost.isNotNullNorEmpty()
&& webType.isNotNullNorEmpty()
&& symbol.isNotNullNorEmpty()
fun isApiLoginValid() = currentSemesterEndDate-30 > currentTimeUnix()
&& apiFingerprint[symbol].isNotNullNorEmpty()

View File

@ -19,6 +19,7 @@ const val ENDPOINT_VULCAN_API_NOTICES = 1070
const val ENDPOINT_VULCAN_API_ATTENDANCE = 1080
const val ENDPOINT_VULCAN_API_MESSAGES_INBOX = 1090
const val ENDPOINT_VULCAN_API_MESSAGES_SENT = 1100
const val ENDPOINT_VULCAN_WEB_LUCKY_NUMBERS = 2010
val VulcanFeatures = listOf(
// timetable
@ -61,6 +62,13 @@ val VulcanFeatures = listOf(
!data.app.config.sync.tokenVulcanList.contains(data.profileId)
},
/**
* Lucky number - using WEB Main.
*/
Feature(LOGIN_TYPE_VULCAN, FEATURE_LUCKY_NUMBER, listOf(
ENDPOINT_VULCAN_WEB_LUCKY_NUMBERS to LOGIN_METHOD_VULCAN_WEB_MAIN
), listOf(LOGIN_METHOD_VULCAN_WEB_MAIN)).withShouldSync { data -> data.shouldSyncLuckyNumber() },
Feature(LOGIN_TYPE_VULCAN, FEATURE_ALWAYS_NEEDED, listOf(
ENDPOINT_VULCAN_API_UPDATE_SEMESTER to LOGIN_METHOD_VULCAN_API,
ENDPOINT_VULCAN_API_DICTIONARIES to LOGIN_METHOD_VULCAN_API

View File

@ -7,6 +7,7 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.*
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.api.*
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.web.VulcanWebLuckyNumber
import pl.szczodrzynski.edziennik.utils.Utils
class VulcanData(val data: DataVulcan, val onSuccess: () -> Unit) {
@ -86,6 +87,10 @@ class VulcanData(val data: DataVulcan, val onSuccess: () -> Unit) {
data.startProgress(R.string.edziennik_progress_endpoint_messages_outbox)
VulcanApiMessagesSent(data, lastSync, onSuccess)
}
ENDPOINT_VULCAN_WEB_LUCKY_NUMBERS -> {
data.startProgress(R.string.edziennik_progress_endpoint_lucky_number)
VulcanWebLuckyNumber(data, lastSync, onSuccess)
}
else -> onSuccess(endpointId)
}
}

View File

@ -44,9 +44,9 @@ open class VulcanWebMain(open val data: DataVulcan, open val lastSync: Long?) {
Jspoon.create().adapter(CufsCertificate::class.java)
}
fun saveCertificate(certificate: String) {
fun saveCertificate(xml: String) {
val file = File(data.app.filesDir, "cert_"+(data.webUsername ?: data.webEmail)+".xml")
file.writeText(certificate)
file.writeText(xml)
}
fun readCertificate(): String? {
@ -56,20 +56,20 @@ open class VulcanWebMain(open val data: DataVulcan, open val lastSync: Long?) {
return null
}
fun parseCertificate(certificate: String): CufsCertificate {
val xml = certificate
fun parseCertificate(xml: String): CufsCertificate {
val xmlParsed = xml
.replace("<[a-z]+?:".toRegex(), "<")
.replace("</[a-z]+?:".toRegex(), "</")
.replace("\\sxmlns.*?=\".+?\"".toRegex(), "")
return certificateAdapter.fromHtml(xml)
return certificateAdapter.fromHtml(xmlParsed).also {
it.xml = xml
}
}
fun postCertificate(certificate: String, symbol: String, onResult: (symbol: String, state: Int) -> Unit): Boolean {
val cufsCertificate = parseCertificate(certificate)
fun postCertificate(certificate: CufsCertificate, symbol: String, onResult: (symbol: String, state: Int) -> Unit): Boolean {
// check if the certificate is valid
if (Date.fromIso(cufsCertificate.expiryDate) < System.currentTimeMillis())
if (Date.fromIso(certificate.expiryDate) < System.currentTimeMillis())
return false
val callback = object : TextCallbackHandler() {
@ -86,6 +86,7 @@ open class VulcanWebMain(open val data: DataVulcan, open val lastSync: Long?) {
if (!validateCallback(text, response, jsonResponse = false)) {
return
}
data.webExpiryTime = Date.fromIso(certificate.expiryDate) / 1000L
onResult(symbol, STATE_SUCCESS)
}
@ -102,8 +103,8 @@ open class VulcanWebMain(open val data: DataVulcan, open val lastSync: Long?) {
.userAgent(SYSTEM_USER_AGENT)
.post()
.addParameter("wa", "wsignin1.0")
.addParameter("wctx", cufsCertificate.targetUrl)
.addParameter("wresult", certificate)
.addParameter("wctx", certificate.targetUrl)
.addParameter("wresult", certificate.xml)
.allowErrorCode(HttpURLConnection.HTTP_BAD_REQUEST)
.allowErrorCode(HttpURLConnection.HTTP_FORBIDDEN)
.allowErrorCode(HttpURLConnection.HTTP_UNAUTHORIZED)
@ -138,7 +139,7 @@ open class VulcanWebMain(open val data: DataVulcan, open val lastSync: Long?) {
data.webPermissions = Regexes.VULCAN_WEB_PERMISSIONS.find(text)?.let { it[1] }
val schoolSymbols = mutableListOf<String>()
val clientUrl = "https://uonetplus-uczen.${data.webHost}/$symbol/"
val clientUrl = "://uonetplus-uczen.${data.webHost}/$symbol/"
var clientIndex = text.indexOf(clientUrl)
var count = 0
while (clientIndex != -1 && count < 100) {
@ -149,6 +150,17 @@ open class VulcanWebMain(open val data: DataVulcan, open val lastSync: Long?) {
clientIndex = text.indexOf(clientUrl, startIndex = endIndex)
count++
}
schoolSymbols.removeAll {
it.toLowerCase() == "default"
|| !it.matches(Regexes.VULCAN_WEB_SYMBOL_VALIDATE)
}
if (postErrors && schoolSymbols.isEmpty()) {
data.error(ApiError(TAG, ERROR_VULCAN_WEB_NO_SCHOOLS)
.withResponse(response)
.withApiResponse(text))
return
}
onSuccess(text, schoolSymbols)
}
@ -209,7 +221,7 @@ open class VulcanWebMain(open val data: DataVulcan, open val lastSync: Long?) {
webType: Int,
endpoint: String,
method: Int = POST,
parameters: Map<String, Any> = emptyMap(),
parameters: Map<String, Any?> = emptyMap(),
onSuccess: (json: JsonObject, response: Response?) -> Unit
) {
val url = "https://" + when (webType) {

View File

@ -0,0 +1,16 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-4-20.
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.web
import com.google.gson.annotations.SerializedName
data class HomepageTile(
@SerializedName("Nazwa")
val name: String?,
@SerializedName("Url")
val url: String?,
@SerializedName("Zawartosc")
val children: List<HomepageTile>
)

View File

@ -0,0 +1,87 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-4-20.
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.web
import pl.szczodrzynski.edziennik.DAY
import pl.szczodrzynski.edziennik.data.api.VULCAN_WEB_ENDPOINT_LUCKY_NUMBER
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.ENDPOINT_VULCAN_WEB_LUCKY_NUMBERS
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanWebMain
import pl.szczodrzynski.edziennik.data.db.entity.LuckyNumber
import pl.szczodrzynski.edziennik.data.db.entity.Metadata
import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS
import pl.szczodrzynski.edziennik.getJsonArray
import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.edziennik.utils.models.Time
import pl.szczodrzynski.edziennik.utils.models.Week
class VulcanWebLuckyNumber(override val data: DataVulcan,
override val lastSync: Long?,
val onSuccess: (endpointId: Int) -> Unit
) : VulcanWebMain(data, lastSync) {
companion object {
const val TAG = "VulcanWebLuckyNumber"
}
init {
webGetJson(TAG, WEB_MAIN, VULCAN_WEB_ENDPOINT_LUCKY_NUMBER, parameters = mapOf(
"permissions" to data.webPermissions
)) { json, _ ->
val tiles = json
.getJsonArray("data")
?.mapNotNull { data.app.gson.fromJson(it.toString(), HomepageTile::class.java) }
?.flatMap { it.children }
if (tiles == null) {
data.setSyncNext(ENDPOINT_VULCAN_WEB_LUCKY_NUMBERS, SYNC_ALWAYS)
onSuccess(ENDPOINT_VULCAN_WEB_LUCKY_NUMBERS)
return@webGetJson
}
var nextSync = System.currentTimeMillis() + 1* DAY *1000
tiles.firstOrNull { it.name == data.schoolShort }?.children?.firstOrNull()?.let { tile ->
// "Szczęśliwy numer w dzienniku: 16"
return@let tile.name?.substringAfterLast(' ')?.toIntOrNull()?.let { number ->
// lucky number present
val luckyNumberObject = LuckyNumber(
profileId,
Date.getToday(),
number
)
data.luckyNumberList.add(luckyNumberObject)
data.metadataList.add(
Metadata(
profileId,
Metadata.TYPE_LUCKY_NUMBER,
luckyNumberObject.date.value.toLong(),
true,
profile?.empty ?: false,
System.currentTimeMillis()
))
}
} ?: {
// no lucky number
if (Date.getToday().weekDay <= Week.FRIDAY && Time.getNow().hour >= 22) {
// working days, after 10PM
// consider the lucky number is disabled; sync in 4 days
nextSync = System.currentTimeMillis() + 4*DAY*1000
}
else if (Date.getToday().weekDay <= Week.FRIDAY && Time.getNow().hour < 22) {
// working days, before 10PM
}
else {
// weekends
nextSync = Week.getNearestWeekDayDate(Week.MONDAY).combineWith(Time(5, 0, 0))
}
}()
data.setSyncNext(ENDPOINT_VULCAN_WEB_LUCKY_NUMBERS, SYNC_ALWAYS)
onSuccess(ENDPOINT_VULCAN_WEB_LUCKY_NUMBERS)
}
}
}

View File

@ -10,6 +10,7 @@ import pl.szczodrzynski.edziennik.data.api.*
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanApi
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanWebMain
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.login.CufsCertificate
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.login.VulcanLoginApi
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.login.VulcanLoginWebMain
import pl.szczodrzynski.edziennik.data.api.events.FirstLoginFinishedEvent
@ -32,17 +33,18 @@ class VulcanFirstLogin(val data: DataVulcan, val onSuccess: () -> Unit) {
init {
if (data.loginStore.mode == LOGIN_MODE_VULCAN_WEB) {
VulcanLoginWebMain(data) {
val certificate = web.readCertificate() ?: run {
val xml = web.readCertificate() ?: run {
data.error(ApiError(TAG, ERROR_VULCAN_WEB_NO_CERTIFICATE))
return@VulcanLoginWebMain
}
val certificate = web.parseCertificate(xml)
if (data.symbol != null && data.symbol != "default") {
tryingSymbols += data.symbol ?: "default"
}
else {
val cufsCertificate = web.parseCertificate(certificate)
tryingSymbols += cufsCertificate.userInstances
tryingSymbols += certificate.userInstances
}
checkSymbol(certificate)
@ -56,7 +58,7 @@ class VulcanFirstLogin(val data: DataVulcan, val onSuccess: () -> Unit) {
}
}
private fun checkSymbol(certificate: String) {
private fun checkSymbol(certificate: CufsCertificate) {
if (tryingSymbols.isEmpty()) {
EventBus.getDefault().postSticky(FirstLoginFinishedEvent(profileList, data.loginStore))
onSuccess()
@ -80,12 +82,16 @@ class VulcanFirstLogin(val data: DataVulcan, val onSuccess: () -> Unit) {
// postCertificate returns false if the cert is not valid anymore
if (!result) {
data.error(ApiError(TAG, ERROR_VULCAN_WEB_CERTIFICATE_EXPIRED)
.withApiResponse(certificate))
.withApiResponse(certificate.xml))
}
}
private fun webRegisterDevice(symbol: String, onSuccess: () -> Unit) {
web.getStartPage(symbol, postErrors = false) { _, schoolSymbols ->
if (schoolSymbols.isEmpty()) {
onSuccess()
return@getStartPage
}
data.symbol = symbol
val schoolSymbol = data.schoolSymbol ?: schoolSymbols.firstOrNull()
web.webGetJson(TAG, VulcanWebMain.WEB_NEW, "$schoolSymbol/$VULCAN_WEB_ENDPOINT_REGISTER_DEVICE") { result, _ ->

View File

@ -18,4 +18,6 @@ class CufsCertificate {
@Selector(value = "Attribute[AttributeName=UserInstance] AttributeValue")
var userInstances: List<String> = listOf()
var xml = ""
}

View File

@ -11,6 +11,7 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanWebMain
import pl.szczodrzynski.edziennik.data.api.models.ApiError
import pl.szczodrzynski.edziennik.getString
import pl.szczodrzynski.edziennik.isNotNullNorEmpty
import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.fslogin.FSLogin
import pl.szczodrzynski.fslogin.realm.CufsRealm
@ -82,35 +83,20 @@ class VulcanLoginWebMain(val data: DataVulcan, val onSuccess: () -> Unit) {
else -> return false
}
val certificate = web.readCertificate()?.let { web.parseCertificate(it) }
if (certificate != null && Date.fromIso(certificate.expiryDate) > System.currentTimeMillis()) {
useCertificate(certificate)
return true
}
val fsLogin = FSLogin(data.app.http, debug = App.debugMode)
fsLogin.performLogin(
realm = realm,
username = data.webUsername ?: data.webEmail ?: return false,
password = data.webPassword ?: return false,
onSuccess = { certificate ->
web.saveCertificate(certificate.wresult)
// auto-post certificate when not first login
if (data.profile != null && data.symbol != null && data.symbol != "default") {
val result = web.postCertificate(certificate.wresult, data.symbol ?: "default") { _, state ->
when (state) {
VulcanWebMain.STATE_SUCCESS -> {
web.getStartPage { _, _ -> onSuccess() }
}
VulcanWebMain.STATE_NO_REGISTER -> data.error(ApiError(TAG, ERROR_VULCAN_WEB_NO_REGISTER))
VulcanWebMain.STATE_LOGGED_OUT -> data.error(ApiError(TAG, ERROR_VULCAN_WEB_LOGGED_OUT))
}
}
// postCertificate returns false if the cert is not valid anymore
if (!result) {
data.error(ApiError(TAG, ERROR_VULCAN_WEB_CERTIFICATE_EXPIRED)
.withApiResponse(certificate.wresult))
}
}
else {
// first login - succeed immediately
onSuccess()
}
onSuccess = { fsCertificate ->
web.saveCertificate(fsCertificate.wresult)
useCertificate(web.parseCertificate(fsCertificate.wresult))
},
onFailure = { errorText ->
// TODO
@ -120,4 +106,28 @@ class VulcanLoginWebMain(val data: DataVulcan, val onSuccess: () -> Unit) {
return true
}
private fun useCertificate(certificate: CufsCertificate) {
// auto-post certificate when not first login
if (data.profile != null && data.symbol != null && data.symbol != "default") {
val result = web.postCertificate(certificate, data.symbol ?: "default") { _, state ->
when (state) {
VulcanWebMain.STATE_SUCCESS -> {
web.getStartPage { _, _ -> onSuccess() }
}
VulcanWebMain.STATE_NO_REGISTER -> data.error(ApiError(TAG, ERROR_VULCAN_WEB_NO_REGISTER))
VulcanWebMain.STATE_LOGGED_OUT -> data.error(ApiError(TAG, ERROR_VULCAN_WEB_LOGGED_OUT))
}
}
// postCertificate returns false if the cert is not valid anymore
if (!result) {
data.error(ApiError(TAG, ERROR_VULCAN_WEB_CERTIFICATE_EXPIRED)
.withApiResponse(certificate.xml))
}
}
else {
// first login - succeed immediately
onSuccess()
}
}
}