[APIv2/Librus] Add Librus Synergia login method. Update structure and error handling.

This commit is contained in:
Kuba Szczodrzyński 2019-09-22 22:02:36 +02:00
parent 76d39ac623
commit a785db4d47
13 changed files with 378 additions and 128 deletions

View File

@ -8,6 +8,7 @@ import android.os.Build
import android.os.Bundle
import androidx.core.app.ActivityCompat
import com.google.gson.JsonArray
import com.google.gson.JsonElement
import com.google.gson.JsonObject
import pl.szczodrzynski.edziennik.datamodels.Profile
import pl.szczodrzynski.edziennik.datamodels.Teacher
@ -21,19 +22,21 @@ fun List<Teacher>.byNameLastFirst(nameLastFirst: String) = firstOrNull { it.surn
fun List<Teacher>.byNameFDotLast(nameFDotLast: String) = firstOrNull { it.name + "." + it.surname == nameFDotLast }
fun List<Teacher>.byNameFDotSpaceLast(nameFDotSpaceLast: String) = firstOrNull { it.name + ". " + it.surname == nameFDotSpaceLast }
fun JsonObject.getBoolean(key: String): Boolean? = get(key)?.let { if (it.isJsonNull) null else it.asBoolean }
fun JsonObject.getString(key: String): String? = get(key)?.let { if (it.isJsonNull) null else it.asString }
fun JsonObject.getInt(key: String): Int? = get(key)?.let { if (it.isJsonNull) null else it.asInt }
fun JsonObject.getLong(key: String): Long? = get(key)?.let { if (it.isJsonNull) null else it.asLong }
fun JsonObject.getJsonObject(key: String): JsonObject? = get(key)?.let { if (it.isJsonNull) null else it.asJsonObject }
fun JsonObject.getJsonArray(key: String): JsonArray? = get(key)?.let { if (it.isJsonNull) null else it.asJsonArray }
fun JsonObject?.get(key: String): JsonElement? = this?.get(key)
fun JsonObject.getBoolean(key: String, defaultValue: Boolean): Boolean = get(key)?.let { if (it.isJsonNull) defaultValue else it.asBoolean } ?: defaultValue
fun JsonObject.getString(key: String, defaultValue: String): String = get(key)?.let { if (it.isJsonNull) defaultValue else it.asString } ?: defaultValue
fun JsonObject.getInt(key: String, defaultValue: Int): Int = get(key)?.let { if (it.isJsonNull) defaultValue else it.asInt } ?: defaultValue
fun JsonObject.getLong(key: String, defaultValue: Long): Long = get(key)?.let { if (it.isJsonNull) defaultValue else it.asLong } ?: defaultValue
fun JsonObject.getJsonObject(key: String, defaultValue: JsonObject): JsonObject = get(key)?.let { if (it.isJsonNull) defaultValue else it.asJsonObject } ?: defaultValue
fun JsonObject.getJsonArray(key: String, defaultValue: JsonArray): JsonArray = get(key)?.let { if (it.isJsonNull) defaultValue else it.asJsonArray } ?: defaultValue
fun JsonObject?.getBoolean(key: String): Boolean? = get(key)?.let { if (it.isJsonNull) null else it.asBoolean }
fun JsonObject?.getString(key: String): String? = get(key)?.let { if (it.isJsonNull) null else it.asString }
fun JsonObject?.getInt(key: String): Int? = get(key)?.let { if (it.isJsonNull) null else it.asInt }
fun JsonObject?.getLong(key: String): Long? = get(key)?.let { if (it.isJsonNull) null else it.asLong }
fun JsonObject?.getJsonObject(key: String): JsonObject? = get(key)?.let { if (it.isJsonNull) null else it.asJsonObject }
fun JsonObject?.getJsonArray(key: String): JsonArray? = get(key)?.let { if (it.isJsonNull) null else it.asJsonArray }
fun JsonObject?.getBoolean(key: String, defaultValue: Boolean): Boolean = get(key)?.let { if (it.isJsonNull) defaultValue else it.asBoolean } ?: defaultValue
fun JsonObject?.getString(key: String, defaultValue: String): String = get(key)?.let { if (it.isJsonNull) defaultValue else it.asString } ?: defaultValue
fun JsonObject?.getInt(key: String, defaultValue: Int): Int = get(key)?.let { if (it.isJsonNull) defaultValue else it.asInt } ?: defaultValue
fun JsonObject?.getLong(key: String, defaultValue: Long): Long = get(key)?.let { if (it.isJsonNull) defaultValue else it.asLong } ?: defaultValue
fun JsonObject?.getJsonObject(key: String, defaultValue: JsonObject): JsonObject = get(key)?.let { if (it.isJsonNull) defaultValue else it.asJsonObject } ?: defaultValue
fun JsonObject?.getJsonArray(key: String, defaultValue: JsonArray): JsonArray = get(key)?.let { if (it.isJsonNull) defaultValue else it.asJsonArray } ?: defaultValue
fun CharSequence?.isNotNullNorEmpty(): Boolean {
return this != null && this.isNotEmpty()

View File

@ -85,10 +85,10 @@ public class AppError {
this(TAG, line, errorCode, null, null, null, throwable, apiResponse);
}
public AppError(String TAG, int line, int errorCode, Throwable throwable, JsonObject apiResponse) {
this(TAG, line, errorCode, null, null, null, throwable, apiResponse.toString());
this(TAG, line, errorCode, null, null, null, throwable, apiResponse == null ? null : apiResponse.toString());
}
public AppError(String TAG, int line, int errorCode, String errorText, Response response, JsonObject apiResponse) {
this(TAG, line, errorCode, errorText, response, response == null ? null : response.request(), null, apiResponse.toString());
this(TAG, line, errorCode, errorText, response, response == null ? null : response.request(), null, apiResponse == null ? null : apiResponse.toString());
}
public AppError(String TAG, int line, int errorCode, String errorText, Response response, String apiResponse) {
this(TAG, line, errorCode, errorText, response, response == null ? null : response.request(), null, apiResponse);
@ -97,7 +97,7 @@ public class AppError {
this(TAG, line, errorCode, errorText, null, null, null, apiResponse);
}
public AppError(String TAG, int line, int errorCode, String errorText, JsonObject apiResponse) {
this(TAG, line, errorCode, errorText, null, null, null, apiResponse.toString());
this(TAG, line, errorCode, errorText, null, null, null, apiResponse == null ? null : apiResponse.toString());
}
public AppError(String TAG, int line, int errorCode, String errorText) {
this(TAG, line, errorCode, errorText, null, null, null, null);
@ -106,7 +106,7 @@ public class AppError {
this(TAG, line, errorCode, null, null, null, null, apiResponse.toString());
}
public AppError(String TAG, int line, int errorCode, Response response, Throwable throwable, JsonObject apiResponse) {
this(TAG, line, errorCode, null, response, response == null ? null : response.request(), throwable, apiResponse.toString());
this(TAG, line, errorCode, null, response, response == null ? null : response.request(), throwable, apiResponse == null ? null : apiResponse.toString());
}
public AppError(String TAG, int line, int errorCode, Response response, Throwable throwable, String apiResponse) {
this(TAG, line, errorCode, null, response, response == null ? null : response.request(), throwable, apiResponse);
@ -115,7 +115,7 @@ public class AppError {
this(TAG, line, errorCode, null, response, response == null ? null : response.request(), null, apiResponse);
}
public AppError(String TAG, int line, int errorCode, Response response, JsonObject apiResponse) {
this(TAG, line, errorCode, null, response, response == null ? null : response.request(), null, apiResponse.toString());
this(TAG, line, errorCode, null, response, response == null ? null : response.request(), null, apiResponse == null ? null : apiResponse.toString());
}
public String getDetails(Context context) {

View File

@ -27,8 +27,11 @@ const val LIBRUS_TOKEN_URL = "https://portal.librus.pl/oauth2/access_token"
const val LIBRUS_ACCOUNT_URL = "https://portal.librus.pl/api/v2/SynergiaAccounts/fresh/" // + login
const val LIBRUS_ACCOUNTS_URL = "https://portal.librus.pl/api/v2/SynergiaAccounts"
const val LIBRUS_API_URL = "https://api.librus.pl/2.0/"
/** https://api.librus.pl/2.0 */
const val LIBRUS_API_URL = "https://api.librus.pl/2.0"
/** https://api.librus.pl/OAuth/Token */
const val LIBRUS_API_TOKEN_URL = "https://api.librus.pl/OAuth/Token"
/** https://api.librus.pl/OAuth/TokenJST */
const val LIBRUS_API_TOKEN_JST_URL = "https://api.librus.pl/OAuth/TokenJST"
const val LIBRUS_API_AUTHORIZATION = "Mjg6ODRmZGQzYTg3YjAzZDNlYTZmZmU3NzdiNThiMzMyYjE="
const val LIBRUS_API_SECRET_JST = "18b7c1ee08216f636a1b1a2440e68398"
@ -38,7 +41,8 @@ const val LIBRUS_API_CLIENT_ID_JST = "49"
const val LIBRUS_JST_DEMO_CODE = "68656A21"
const val LIBRUS_JST_DEMO_PIN = "1290"
const val LIBRUS_SYNERGIA_TOKEN_LOGIN_URL = "https://synergia.librus.pl/loguj/token/\$token/przenies/"
/** https://synergia.librus.pl/loguj/token/TOKEN/przenies */
const val LIBRUS_SYNERGIA_TOKEN_LOGIN_URL = "https://synergia.librus.pl/loguj/token/TOKEN/przenies/"
const val LIBRUS_MESSAGES_URL = "https://wiadomosci.librus.pl/module/"
const val LIBRUS_SANDBOX_URL = "https://sandbox.librus.pl/index.php?action="

View File

@ -30,12 +30,58 @@ const val CODE_LIBRUS_NOT_ACTIVATED = 29
const val CODE_SYNERGIA_NOT_ACTIVATED = 32
const val CODE_LIBRUS_DISCONNECTED = 31
const val CODE_PROFILE_ARCHIVED = 30*/
const val CODE_INVALID_LOGIN_MODE = 130
const val CODE_INTERNAL_MISSING_DATA = 100
const val ERROR_REQUEST_FAILURE = 50
const val ERROR_REQUEST_HTTP_400 = 51
const val ERROR_REQUEST_HTTP_401 = 52
const val ERROR_REQUEST_HTTP_403 = 53
const val ERROR_REQUEST_HTTP_404 = 54
const val ERROR_REQUEST_HTTP_405 = 55
const val ERROR_REQUEST_HTTP_410 = 56
const val ERROR_REQUEST_HTTP_500 = 57
const val ERROR_RESPONSE_EMPTY = 100
const val ERROR_LOGIN_DATA_MISSING = 101
const val ERROR_LOGIN_DATA_INVALID = 102
const val ERROR_PROFILE_MISSING = 105
const val ERROR_INVALID_LOGIN_MODE = 110
const val ERROR_LOGIN_METHOD_NOT_SATISFIED = 111
const val CODE_INTERNAL_LIBRUS_ACCOUNT_410 = 120
const val CODE_INTERNAL_LIBRUS_SYNERGIA_EXPIRED = 121
const val CODE_LOGIN_METHOD_NOT_SATISFIED = 122
const val CODE_LIBRUS_PROFILE_NULL = 123
const val ERROR_LOGIN_LIBRUS_API_CAPTCHA_NEEDED = 124
const val ERROR_LOGIN_LIBRUS_API_CONNECTION_PROBLEMS = 125
const val ERROR_LOGIN_LIBRUS_API_INVALID_CLIENT = 126
const val ERROR_LOGIN_LIBRUS_API_REG_ACCEPT_NEEDED = 127
const val ERROR_LOGIN_LIBRUS_API_CHANGE_PASSWORD_ERROR = 128
const val ERROR_LOGIN_LIBRUS_API_PASSWORD_CHANGE_REQUIRED = 129
const val ERROR_LOGIN_LIBRUS_API_INVALID_GRANT = 130
const val ERROR_LOGIN_LIBRUS_API_OTHER = 131
const val ERROR_LOGIN_LIBRUS_PORTAL_CSRF_MISSING = 132
const val ERROR_LOGIN_LIBRUS_PORTAL_NOT_ACTIVATED = 133
const val ERROR_LOGIN_LIBRUS_PORTAL_ACTION_ERROR = 134
const val ERROR_LOGIN_LIBRUS_PORTAL_TOKEN_ERROR = 135
const val ERROR_LOGIN_LIBRUS_PORTAL_SYNERGIA_DISCONNECTED = 136
const val ERROR_LOGIN_LIBRUS_PORTAL_SYNERGIA_410 = 137
const val ERROR_LOGIN_LIBRUS_PORTAL_SYNERGIA_NOT_FOUND = 138
const val ERROR_LOGIN_LIBRUS_PORTAL_SYNERGIA_OTHER = 139
const val ERROR_LOGIN_LIBRUS_PORTAL_SYNERGIA_TOKEN_MISSING = 139
const val ERROR_LIBRUS_API_TOKEN_EXPIRED = 140
const val ERROR_LIBRUS_API_INSUFFICIENT_SCOPES = 141
const val ERROR_LIBRUS_API_OTHER = 142
const val ERROR_LIBRUS_API_REQUEST_DENIED = 143
const val ERROR_LIBRUS_API_RESOURCE_NOT_FOUND = 144
const val ERROR_LIBRUS_API_DATA_NOT_FOUND = 145
const val ERROR_LIBRUS_API_TIMETABLE_NOT_PUBLIC = 146
const val ERROR_LIBRUS_API_RESOURCE_ACCESS_DENIED = 147
const val ERROR_LIBRUS_API_INVALID_REQUEST_PARAMS = 148
const val ERROR_LIBRUS_API_INCORRECT_ENDPOINT = 149
const val ERROR_LIBRUS_API_LUCKY_NUMBER_NOT_ACTIVE = 150
const val ERROR_LIBRUS_API_NOTES_NOT_ACTIVE = 151
const val ERROR_LOGIN_LIBRUS_SYNERGIA_NO_TOKEN = 152
const val ERROR_LOGIN_LIBRUS_SYNERGIA_TOKEN_INVALID = 153
const val ERROR_LOGIN_LIBRUS_SYNERGIA_NO_SESSION_ID = 154
const val EXCEPTION_LOGIN_LIBRUS_API_TOKEN = 901
const val EXCEPTION_LOGIN_LIBRUS_PORTAL_TOKEN = 902
const val EXCEPTION_LOGIN_LIBRUS_PORTAL_SYNERGIA_TOKEN = 903
const val EXCEPTION_LIBRUS_API_REQUEST = 904

View File

@ -23,17 +23,17 @@ class LibrusTest(val app: App) {
}
val profile = Profile(1, "Profil", "xd", 1).apply {
//putStudentData("accountLogin", "1234567")
putStudentData("accountLogin", "1234567")
//putStudentData("accountPassword", "zaq1@WSX")
putStudentData("accountCode", LIBRUS_JST_DEMO_CODE)
putStudentData("accountPin", LIBRUS_JST_DEMO_PIN)
//putStudentData("accountCode", LIBRUS_JST_DEMO_CODE)
//putStudentData("accountPin", LIBRUS_JST_DEMO_PIN)
}
val loginStore = LoginStore(1, LOGIN_TYPE_LIBRUS, JsonObject().apply {
addProperty("email", "test@example.com")
addProperty("password", "zaq1@WSX")
}).also {
it.mode = LOGIN_MODE_LIBRUS_JST
it.mode = LOGIN_MODE_LIBRUS_EMAIL
}
fun go() {
@ -54,7 +54,7 @@ class LibrusTest(val app: App) {
}
}
LoginLibrus(data, LOGIN_METHOD_LIBRUS_API) {
LoginLibrus(data, LOGIN_METHOD_LIBRUS_SYNERGIA) {
d(TAG, "Login succeeded.")
d(TAG, "Profile data: ${data.profile?.studentData?.toString()}")
d(TAG, "LoginStore data: ${data.loginStore.data}")

View File

@ -49,6 +49,7 @@ class DataLibrus(app: App, profile: Profile?, loginStore: LoginStore) : Data(app
/**
* A Synergia login, like 1234567u.
* Used: for login (API Login Method) in Synergia mode.
* Used: for login (Synergia Login Method) in Synergia mode.
* And also in various places in [pl.szczodrzynski.edziennik.api.v2.models.Endpoint]s
*/
private var mApiLogin: String? = null
@ -58,6 +59,7 @@ class DataLibrus(app: App, profile: Profile?, loginStore: LoginStore) : Data(app
/**
* A Synergia password.
* Used: for login (API Login Method) in Synergia mode.
* Used: for login (Synergia Login Method) in Synergia mode.
*/
private var mApiPassword: String? = null
var apiPassword: String?
@ -109,6 +111,32 @@ class DataLibrus(app: App, profile: Profile?, loginStore: LoginStore) : Data(app
get() { mApiTokenExpiryTime = mApiTokenExpiryTime ?: profile?.getStudentData("accountTokenTime", 0L); return mApiTokenExpiryTime ?: 0L }
set(value) { profile?.putStudentData("accountTokenTime", value) ?: return; mApiTokenExpiryTime = value }
/* _____ _
/ ____| (_)
| (___ _ _ _ __ ___ _ __ __ _ _ __ _
\___ \| | | | '_ \ / _ \ '__/ _` | |/ _` |
____) | |_| | | | | __/ | | (_| | | (_| |
|_____/ \__, |_| |_|\___|_| \__, |_|\__,_|
__/ | __/ |
|___/ |__*/
/**
* A Synergia web Session ID (DZIENNIKSID).
* Used in endpoints with Synergia login method.
*/
private var mSynergiaSessionId: String? = null
var synergiaSessionId: String?
get() { mSynergiaSessionId = mSynergiaSessionId ?: profile?.getStudentData("accountSID", null); return mSynergiaSessionId }
set(value) { profile?.putStudentData("accountSID", value) ?: return; mSynergiaSessionId = value }
/**
* The expiry time for [synergiaSessionId], as a UNIX timestamp.
* Used in endpoints with Synergia login method.
* TODO verify how long is the session ID valid.
*/
private var mSynergiaSessionIdExpiryTime: Long? = null
var synergiaSessionIdExpiryTime: Long
get() { mSynergiaSessionIdExpiryTime = mSynergiaSessionIdExpiryTime ?: profile?.getStudentData("accountSIDTime", 0L); return mSynergiaSessionIdExpiryTime ?: 0L }
set(value) { profile?.putStudentData("accountSIDTime", value) ?: return; mSynergiaSessionIdExpiryTime = value }
/* ____ _ _
/ __ \| | | |
| | | | |_| |__ ___ _ __

View File

@ -24,9 +24,9 @@ open class LibrusApi(override val data: DataLibrus) : Api(data) {
const val TAG = "LibrusApi"
}
fun apiRequest(endpoint: String, callback: (json: JsonObject?) -> Unit) {
d(TAG, "Requesting $LIBRUS_API_URL$endpoint")
d(TAG, "Requesting $LIBRUS_API_URL/$endpoint")
Request.builder()
.url(if (data.fakeLogin) "http://szkolny.eu/librus/api/$endpoint" else LIBRUS_API_URL + endpoint)
.url(if (data.fakeLogin) "http://szkolny.eu/librus/api/$endpoint" else "$LIBRUS_API_URL/$endpoint")
.userAgent(LIBRUS_USER_AGENT)
.addHeader("Authorization", "Bearer ${data.apiAccessToken}")
.get()

View File

@ -63,7 +63,7 @@ class LoginLibrus(val data: DataLibrus, vararg loginMethodIds: Int, val onSucces
}
}
LOGIN_METHOD_LIBRUS_SYNERGIA -> {
LoginLibrusApi(data) {
LoginLibrusSynergia(data) {
data.loginMethods.add(loginMethodId)
onSuccess()
}

View File

@ -17,7 +17,6 @@ import pl.szczodrzynski.edziennik.currentTimeUnix
import pl.szczodrzynski.edziennik.getInt
import pl.szczodrzynski.edziennik.getString
import pl.szczodrzynski.edziennik.isNotNullNorEmpty
import java.net.HttpURLConnection
import java.net.HttpURLConnection.*
class LoginLibrusApi {
@ -28,12 +27,13 @@ class LoginLibrusApi {
private lateinit var data: DataLibrus
private lateinit var onSuccess: () -> Unit
/* do NOT move this to primary constructor */
constructor(data: DataLibrus, onSuccess: () -> Unit) {
this.data = data
this.onSuccess = onSuccess
if (data.profile == null) {
data.callback.onError(null, AppError(TAG, 19, CODE_LIBRUS_PROFILE_NULL))
data.error(TAG, ERROR_PROFILE_MISSING)
return
}
@ -46,7 +46,7 @@ class LoginLibrusApi {
LOGIN_MODE_LIBRUS_SYNERGIA -> loginWithSynergia()
LOGIN_MODE_LIBRUS_JST -> loginWithJst()
else -> {
data.callback.onError(null, AppError(TAG, 25, CODE_INVALID_LOGIN_MODE))
data.error(TAG, ERROR_INVALID_LOGIN_MODE)
}
}
}
@ -54,7 +54,7 @@ class LoginLibrusApi {
private fun loginWithPortal() {
if (!data.loginMethods.contains(LOGIN_METHOD_LIBRUS_PORTAL)) {
data.callback.onError(null, AppError(TAG, 26, CODE_LOGIN_METHOD_NOT_SATISFIED))
data.error(TAG, ERROR_LOGIN_METHOD_NOT_SATISFIED)
return
}
SynergiaTokenExtractor(data) {
@ -94,7 +94,7 @@ class LoginLibrusApi {
}
else {
// cannot log in: token expired, no login data present
data.callback.onError(null, AppError(TAG, 91, CODE_INVALID_LOGIN))
data.error(TAG, ERROR_LOGIN_DATA_MISSING)
}
}
@ -111,45 +111,31 @@ class LoginLibrusApi {
}
else {
// cannot log in: token expired, no login data present
data.callback.onError(null, AppError(TAG, 110, CODE_INVALID_LOGIN))
data.error(TAG, ERROR_LOGIN_DATA_MISSING)
}
}
private val tokenCallback = object : JsonCallbackHandler() {
override fun onSuccess(json: JsonObject?, response: Response?) {
if (json == null) {
data.callback.onError(null, AppError(TAG, 117, CODE_MAINTENANCE, response))
data.error(TAG, ERROR_RESPONSE_EMPTY, response)
return
}
json.getString("error")?.let { error ->
when (error) {
"librus_captcha_needed" -> {
}
"connection_problems" -> {
}
"invalid_client" -> {
}
"librus_reg_accept_needed" -> {
}
"librus_change_password_error" -> {
}
"librus_password_change_required" -> {
}
"invalid_grant" -> {
}
else -> {
}
}
"librus_captcha_needed" -> ERROR_LOGIN_LIBRUS_API_CAPTCHA_NEEDED
"connection_problems" -> ERROR_LOGIN_LIBRUS_API_CONNECTION_PROBLEMS
"invalid_client" -> ERROR_LOGIN_LIBRUS_API_INVALID_CLIENT
"librus_reg_accept_needed" -> ERROR_LOGIN_LIBRUS_API_REG_ACCEPT_NEEDED
"librus_change_password_error" -> ERROR_LOGIN_LIBRUS_API_CHANGE_PASSWORD_ERROR
"librus_password_change_required" -> ERROR_LOGIN_LIBRUS_API_PASSWORD_CHANGE_REQUIRED
"invalid_grant" -> ERROR_LOGIN_LIBRUS_API_INVALID_GRANT
else -> ERROR_LOGIN_LIBRUS_API_OTHER
}.let { errorCode ->
data.error(TAG, errorCode, apiResponse = json, response = response)
return
}
}
try {
data.apiAccessToken = json.getString("access_token")
@ -157,12 +143,12 @@ class LoginLibrusApi {
data.apiTokenExpiryTime = currentTimeUnix() + json.getInt("expires_in", 86400)
onSuccess()
} catch (e: NullPointerException) {
data.callback.onError(null, AppError(TAG, 154, EXCEPTION_LOGIN_LIBRUS_API_TOKEN, response, e, json))
data.error(TAG, EXCEPTION_LOGIN_LIBRUS_API_TOKEN, response, e, json)
}
}
override fun onFailure(response: Response?, throwable: Throwable?) {
data.callback.onError(null, AppError(TAG, 159, CODE_OTHER, response, throwable))
data.error(TAG, ERROR_REQUEST_FAILURE, response, throwable)
}
}
@ -179,7 +165,6 @@ class LoginLibrusApi {
.contentType(MediaTypeUtils.APPLICATION_FORM)
.post()
.allowErrorCode(HTTP_BAD_REQUEST)
.allowErrorCode(HTTP_FORBIDDEN)
.allowErrorCode(HTTP_UNAUTHORIZED)
.callback(tokenCallback)
.build()
@ -197,7 +182,6 @@ class LoginLibrusApi {
.contentType(MediaTypeUtils.APPLICATION_FORM)
.post()
.allowErrorCode(HTTP_BAD_REQUEST)
.allowErrorCode(HTTP_FORBIDDEN)
.allowErrorCode(HTTP_UNAUTHORIZED)
.callback(tokenCallback)
.build()
@ -218,7 +202,6 @@ class LoginLibrusApi {
.contentType(MediaTypeUtils.APPLICATION_FORM)
.post()
.allowErrorCode(HTTP_BAD_REQUEST)
.allowErrorCode(HTTP_FORBIDDEN)
.allowErrorCode(HTTP_UNAUTHORIZED)
.callback(tokenCallback)
.build()
@ -237,7 +220,6 @@ class LoginLibrusApi {
.contentType(MediaTypeUtils.APPLICATION_FORM)
.post()
.allowErrorCode(HTTP_BAD_REQUEST)
.allowErrorCode(HTTP_FORBIDDEN)
.allowErrorCode(HTTP_UNAUTHORIZED)
.callback(tokenCallback)
.build()

View File

@ -24,11 +24,11 @@ class LoginLibrusPortal(val data: DataLibrus, val onSuccess: () -> Unit) {
init { run {
if (data.loginStore.mode != LOGIN_MODE_LIBRUS_EMAIL) {
data.callback.onError(null, AppError(TAG, 27, CODE_INVALID_LOGIN_MODE))
data.error(TAG, ERROR_INVALID_LOGIN_MODE)
return@run
}
if (data.portalEmail == null || data.portalPassword == null) {
data.callback.onError(null, AppError(TAG, 31, CODE_INVALID_LOGIN))
data.error(TAG, ERROR_LOGIN_DATA_MISSING)
return@run
}
@ -67,13 +67,13 @@ class LoginLibrusPortal(val data: DataLibrus, val onSuccess: () -> Unit) {
if (csrfMatcher.find()) {
login(csrfMatcher.group(1))
} else {
data.callback.onError(null, AppError(TAG, 463, CODE_OTHER, "CSRF token not found.", response, json))
data.error(TAG, ERROR_LOGIN_LIBRUS_PORTAL_CSRF_MISSING, response, json)
}
}
}
override fun onFailure(response: Response, throwable: Throwable) {
data.callback.onError(null, AppError(TAG, 207, CODE_OTHER, response, throwable))
data.error(TAG, ERROR_REQUEST_FAILURE, response, throwable)
}
})
.build()
@ -94,14 +94,14 @@ class LoginLibrusPortal(val data: DataLibrus, val onSuccess: () -> Unit) {
override fun onSuccess(json: JsonObject?, response: Response) {
if (json == null) {
if (response.parserErrorBody?.contains("wciąż nieaktywne") == true) {
data.callback.onError(null, AppError(TAG, 487, CODE_LIBRUS_NOT_ACTIVATED, response))
data.error(TAG, ERROR_LOGIN_LIBRUS_PORTAL_NOT_ACTIVATED, response)
return
}
data.callback.onError(null, AppError(TAG, 489, CODE_MAINTENANCE, response))
data.error(TAG, ERROR_RESPONSE_EMPTY, response)
return
}
if (json.get("errors") != null) {
data.callback.onError(null, AppError(TAG, 490, CODE_OTHER, json.getJsonArray("errors")?.get(0)?.asString, response, json))
data.error(TAG, ERROR_LOGIN_LIBRUS_PORTAL_ACTION_ERROR, response, apiResponse = json)
return
}
authorize(json.getString("redirect", LIBRUS_AUTHORIZE_URL))
@ -109,10 +109,10 @@ class LoginLibrusPortal(val data: DataLibrus, val onSuccess: () -> Unit) {
override fun onFailure(response: Response, throwable: Throwable) {
if (response.code() == 403 || response.code() == 401) {
data.callback.onError(null, AppError(TAG, 248, CODE_INVALID_LOGIN, response, throwable))
data.error(TAG, ERROR_LOGIN_DATA_INVALID, response, throwable)
return
}
data.callback.onError(null, AppError(TAG, 251, CODE_OTHER, response, throwable))
data.error(TAG, ERROR_REQUEST_FAILURE, response, throwable)
}
})
.build()
@ -141,20 +141,19 @@ class LoginLibrusPortal(val data: DataLibrus, val onSuccess: () -> Unit) {
.callback(object : JsonCallbackHandler() {
override fun onSuccess(json: JsonObject?, response: Response) {
if (json == null) {
data.callback.onError(null, AppError(TAG, 539, CODE_MAINTENANCE, response))
data.error(TAG, ERROR_RESPONSE_EMPTY, response)
return
}
json.getString("error")?.let { error ->
val hint = json.getString("hint", "")
val message = json.getString("message", "")
//val message = json.getString("message", "")
if (!refreshTokenFailed && refreshToken != null && (hint == "Token has been revoked" || hint == "Token has expired")) {
c(TAG, "refreshing the token failed. Trying to log in again.")
refreshTokenFailed = true
authorize(LIBRUS_AUTHORIZE_URL)
return
}
val errorText = "$error $message $hint"
data.callback.onError(null, AppError(TAG, 552, CODE_OTHER, errorText, response, json))
data.error(TAG, ERROR_LOGIN_LIBRUS_PORTAL_TOKEN_ERROR, response, apiResponse = json)
return
}
@ -164,13 +163,13 @@ class LoginLibrusPortal(val data: DataLibrus, val onSuccess: () -> Unit) {
data.portalTokenExpiryTime = currentTimeUnix() + json.getInt("expires_in", 86400)
onSuccess()
} catch (e: NullPointerException) {
data.callback.onError(null, AppError(TAG, 311, CODE_OTHER, response, e, json))
data.error(TAG, EXCEPTION_LOGIN_LIBRUS_PORTAL_TOKEN, response, e, json)
}
}
override fun onFailure(response: Response, throwable: Throwable) {
data.callback.onError(null, AppError(TAG, 317, CODE_OTHER, response, throwable))
data.error(TAG, ERROR_REQUEST_FAILURE, response, throwable)
}
})
.build()

View File

@ -4,5 +4,175 @@
package pl.szczodrzynski.edziennik.api.v2.librus.login
class LoginLibrusSynergia {
import com.google.gson.JsonObject
import im.wangchao.mhttp.Request
import im.wangchao.mhttp.Response
import im.wangchao.mhttp.body.MediaTypeUtils
import im.wangchao.mhttp.callback.JsonCallbackHandler
import im.wangchao.mhttp.callback.TextCallbackHandler
import okhttp3.HttpUrl
import pl.szczodrzynski.edziennik.api.v2.*
import pl.szczodrzynski.edziennik.api.v2.librus.data.DataLibrus
import pl.szczodrzynski.edziennik.currentTimeUnix
import pl.szczodrzynski.edziennik.getString
import pl.szczodrzynski.edziennik.isNotNullNorEmpty
import java.lang.Exception
import java.net.HttpURLConnection
class LoginLibrusSynergia(val data: DataLibrus, val onSuccess: () -> Unit) {
companion object {
private const val TAG = "LoginLibrusSynergia"
}
init { run {
if (data.profile == null) {
data.error(TAG, ERROR_PROFILE_MISSING)
return@run
}
if (data.synergiaSessionIdExpiryTime-30 > currentTimeUnix() && data.synergiaSessionId.isNotNullNorEmpty()) {
onSuccess()
}
else {
when (data.loginStore.mode) {
LOGIN_MODE_LIBRUS_SYNERGIA -> loginWithSynergia()
else -> {
loginWithApi()
}
}
}
}}
/**
* HTML form-based login method. Uses a Synergia login and password.
*/
private fun loginWithSynergia() {
if (data.apiLogin == null || data.apiPassword == null) {
data.error(TAG, ERROR_LOGIN_DATA_MISSING)
return
}
}
/**
* A login method using the Synergia API (AutoLoginToken endpoint).
*/
private fun loginWithApi() {
if (!data.loginMethods.contains(LOGIN_METHOD_LIBRUS_API)) {
data.error(TAG, ERROR_LOGIN_METHOD_NOT_SATISFIED)
return
}
val onSuccess = { json: JsonObject ->
loginWithToken(json.getString("Token"))
}
val callback = object : JsonCallbackHandler() {
override fun onSuccess(json: JsonObject?, response: Response?) {
if (json == null && response?.parserErrorBody == null) {
data.error(TAG, ERROR_RESPONSE_EMPTY, response)
return
}
val error = json.getString("Code") ?: json.getString("Message") ?: response?.parserErrorBody
error?.let { code ->
when (code) {
"TokenIsExpired" -> ERROR_LIBRUS_API_TOKEN_EXPIRED
"Insufficient scopes" -> ERROR_LIBRUS_API_INSUFFICIENT_SCOPES
"Request is denied" -> ERROR_LIBRUS_API_REQUEST_DENIED
"Resource not found" -> ERROR_LIBRUS_API_RESOURCE_NOT_FOUND
"NotFound" -> ERROR_LIBRUS_API_DATA_NOT_FOUND
"AccessDeny" -> when (json.getString("Message")) {
"Student timetable is not public" -> ERROR_LIBRUS_API_TIMETABLE_NOT_PUBLIC
else -> ERROR_LIBRUS_API_RESOURCE_ACCESS_DENIED
}
"LuckyNumberIsNotActive" -> ERROR_LIBRUS_API_LUCKY_NUMBER_NOT_ACTIVE
"NotesIsNotActive" -> ERROR_LIBRUS_API_NOTES_NOT_ACTIVE
"InvalidRequest" -> ERROR_LIBRUS_API_INVALID_REQUEST_PARAMS
"Nieprawidłowy węzeł." -> ERROR_LIBRUS_API_INCORRECT_ENDPOINT
else -> ERROR_LIBRUS_API_OTHER
}.let { errorCode ->
data.error(TAG, errorCode, apiResponse = json, response = response)
return
}
}
if (json == null) {
data.error(TAG, ERROR_RESPONSE_EMPTY, response)
return
}
try {
onSuccess(json)
} catch (e: Exception) {
data.error(TAG, EXCEPTION_LIBRUS_API_REQUEST, response, e, json)
}
}
override fun onFailure(response: Response?, throwable: Throwable?) {
// TODO add hotfix for Classrooms 500
data.error(TAG, ERROR_REQUEST_FAILURE, response, throwable)
}
}
Request.builder()
.url("$LIBRUS_API_URL/AutoLoginToken")
.userAgent(LIBRUS_USER_AGENT)
.addHeader("Authorization", "Bearer ${data.apiAccessToken}")
.post()
.allowErrorCode(HttpURLConnection.HTTP_BAD_REQUEST)
.allowErrorCode(HttpURLConnection.HTTP_FORBIDDEN)
.allowErrorCode(HttpURLConnection.HTTP_UNAUTHORIZED)
.callback(callback)
.build()
.enqueue()
}
private fun loginWithToken(token: String?) {
if (token == null) {
data.error(TAG, ERROR_LOGIN_LIBRUS_SYNERGIA_NO_TOKEN)
return
}
val callback = object : TextCallbackHandler() {
override fun onSuccess(json: String?, response: Response?) {
val location = response?.headers()?.get("Location")
if (location?.endsWith("centrum_powiadomien") == true) {
val cookieList = data.app.cookieJar.loadForRequest(HttpUrl.get("https://synergia.librus.pl"))
var sessionId: String? = null
for (cookie in cookieList) {
if (cookie.name().equals("DZIENNIKSID", ignoreCase = true)) {
sessionId = cookie.value()
}
}
if (sessionId == null) {
data.error(TAG, ERROR_LOGIN_LIBRUS_SYNERGIA_NO_SESSION_ID, response, json)
return
}
data.synergiaSessionId = sessionId
data.synergiaSessionIdExpiryTime = currentTimeUnix() + 3600 /* 1h */
onSuccess()
}
else {
data.error(TAG, ERROR_LOGIN_LIBRUS_SYNERGIA_TOKEN_INVALID, response, json)
}
}
override fun onFailure(response: Response?, throwable: Throwable?) {
data.error(TAG, ERROR_REQUEST_FAILURE, response, throwable)
}
}
data.app.cookieJar.clearForDomain("synergia.librus.pl")
Request.builder()
.url(LIBRUS_SYNERGIA_TOKEN_LOGIN_URL.replace("TOKEN", token) + "/uczen/widok/centrum_powiadomien")
.userAgent(LIBRUS_USER_AGENT)
.get()
.allowErrorCode(HttpURLConnection.HTTP_BAD_REQUEST)
.allowErrorCode(HttpURLConnection.HTTP_FORBIDDEN)
.allowErrorCode(HttpURLConnection.HTTP_UNAUTHORIZED)
.callback(callback)
.withClient(data.app.httpLazy)
.build()
.enqueue()
}
}

View File

@ -15,16 +15,16 @@ import java.net.HttpURLConnection.*
class SynergiaTokenExtractor(val data: DataLibrus, val onSuccess: () -> Unit) {
companion object {
private const val TAG = "librus.SynergiaToken"
private const val TAG = "SynergiaTokenExtractor"
}
init { run {
if (data.loginStore.mode != LOGIN_MODE_LIBRUS_EMAIL) {
data.callback.onError(null, AppError(TAG, 23, CODE_INVALID_LOGIN_MODE))
data.error(TAG, ERROR_INVALID_LOGIN_MODE)
return@run
}
if (data.profile == null) {
data.callback.onError(null, AppError(TAG, 28, CODE_LIBRUS_PROFILE_NULL))
data.error(TAG, ERROR_PROFILE_MISSING)
return@run
}
@ -32,7 +32,9 @@ class SynergiaTokenExtractor(val data: DataLibrus, val onSuccess: () -> Unit) {
onSuccess()
}
else {
synergiaAccount()
if (!synergiaAccount()) {
}
}
}}
@ -54,25 +56,25 @@ class SynergiaTokenExtractor(val data: DataLibrus, val onSuccess: () -> Unit) {
.callback(object : JsonCallbackHandler() {
override fun onSuccess(json: JsonObject?, response: Response) {
if (json == null) {
data.callback.onError(null, AppError(TAG, 641, CODE_MAINTENANCE, response))
data.error(TAG, ERROR_RESPONSE_EMPTY, response)
return
}
if (response.code() == 410) {
val reason = json.get("reason")
if (reason != null && reason !is JsonNull && reason.asString == "requires_an_action") {
data.callback.onError(null, AppError(TAG, 1078, CODE_LIBRUS_DISCONNECTED, response, json))
data.error(TAG, ERROR_LOGIN_LIBRUS_PORTAL_SYNERGIA_DISCONNECTED, response, apiResponse = json)
return
}
data.callback.onError(null, AppError(TAG, 70, CODE_INTERNAL_LIBRUS_ACCOUNT_410))
data.error(TAG, ERROR_LOGIN_LIBRUS_PORTAL_SYNERGIA_410, response, apiResponse = json)
return
}
if (json.get("message") != null) {
val message = json.get("message").asString
if (message == "Account not found") {
data.callback.onError(null, AppError(TAG, 651, CODE_OTHER, data.app.getString(R.string.sync_error_register_student_not_associated_format, data.profile?.studentNameLong ?: "", accountLogin), response, json))
data.error(TAG, ERROR_LOGIN_LIBRUS_PORTAL_SYNERGIA_NOT_FOUND, response, apiResponse = json)
return
}
data.callback.onError(null, AppError(TAG, 654, CODE_OTHER, message + "\n\n" + accountLogin, response, json))
data.error(TAG, ERROR_LOGIN_LIBRUS_PORTAL_SYNERGIA_OTHER, response, apiResponse = json)
return
}
if (response.code() == HTTP_OK) {
@ -81,7 +83,7 @@ class SynergiaTokenExtractor(val data: DataLibrus, val onSuccess: () -> Unit) {
val accountId = json.getInt("id")
val accountToken = json.getString("accessToken")
if (accountId == null || accountToken == null) {
data.callback.onError(null, AppError(TAG, 1284, CODE_OTHER, json))
data.error(TAG, ERROR_LOGIN_LIBRUS_PORTAL_SYNERGIA_TOKEN_MISSING, response, apiResponse = json)
return
}
data.apiAccessToken = accountToken
@ -92,16 +94,16 @@ class SynergiaTokenExtractor(val data: DataLibrus, val onSuccess: () -> Unit) {
onSuccess()
} catch (e: NullPointerException) {
e.printStackTrace()
data.callback.onError(null, AppError(TAG, 662, CODE_OTHER, response, e, json))
data.error(TAG, EXCEPTION_LOGIN_LIBRUS_PORTAL_SYNERGIA_TOKEN, response, e, json)
}
} else {
data.callback.onError(null, AppError(TAG, 425, CODE_OTHER, response, json))
data.error(TAG, ERROR_REQUEST_FAILURE, response, apiResponse = json)
}
}
override fun onFailure(response: Response, throwable: Throwable) {
data.callback.onError(null, AppError(TAG, 432, CODE_OTHER, response, throwable))
data.error(TAG, ERROR_REQUEST_FAILURE, response, throwable)
}
})
.build()

View File

@ -3,7 +3,10 @@ package pl.szczodrzynski.edziennik.api.v2.models
import android.util.LongSparseArray
import androidx.core.util.forEach
import androidx.core.util.isNotEmpty
import com.google.gson.JsonObject
import im.wangchao.mhttp.Response
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.api.AppError
import pl.szczodrzynski.edziennik.api.interfaces.ProgressCallback
import pl.szczodrzynski.edziennik.datamodels.*
import pl.szczodrzynski.edziennik.models.Date
@ -14,6 +17,12 @@ open class Data(val app: App, val profile: Profile?, val loginStore: LoginStore)
lateinit var callback: ProgressCallback
/**
* A list of [LoginMethod]s *already fulfilled* during this sync.
*
* A [LoginMethod] may add elements to this list only after a successful login
* with that method.
*/
val loginMethods = mutableListOf<Int>()
val teacherList = LongSparseArray<Teacher>()
@ -133,4 +142,11 @@ open class Data(val app: App, val profile: Profile?, val loginStore: LoginStore)
if (messageMetadataList.isNotEmpty())
db.metadataDao().setSeen(messageMetadataList)
}
fun error(tag: String, errorCode: Int, response: Response? = null, throwable: Throwable? = null, apiResponse: JsonObject? = null) {
callback.onError(null, AppError(tag, 999, errorCode, response, throwable, apiResponse))
}
fun error(tag: String, errorCode: Int, response: Response? = null, apiResponse: String? = null) {
callback.onError(null, AppError(tag, 999, errorCode, response, null, apiResponse))
}
}