[API/Usos] Add base rest API class.

This commit is contained in:
kuba2k2 2022-10-13 21:27:55 +02:00
parent 7935d0f097
commit c7362bce12
No known key found for this signature in database
GPG Key ID: 7919598F25F38819
5 changed files with 266 additions and 2 deletions

View File

@ -27,6 +27,21 @@ class DataUsos(
override fun generateUserCode() = "USOS:TEST" override fun generateUserCode() = "USOS:TEST"
var instanceUrl: String?
get() { mInstanceUrl = mInstanceUrl ?: loginStore.getLoginData("instanceUrl", null); return mInstanceUrl }
set(value) { loginStore.putLoginData("instanceUrl", value); mInstanceUrl = value }
private var mInstanceUrl: String? = null
var oauthConsumerKey: String?
get() { mOauthConsumerKey = mOauthConsumerKey ?: loginStore.getLoginData("oauthConsumerKey", null); return mOauthConsumerKey }
set(value) { loginStore.putLoginData("oauthConsumerKey", value); mOauthConsumerKey = value }
private var mOauthConsumerKey: String? = null
var oauthConsumerSecret: String?
get() { mOauthConsumerSecret = mOauthConsumerSecret ?: loginStore.getLoginData("oauthConsumerSecret", null); return mOauthConsumerSecret }
set(value) { loginStore.putLoginData("oauthConsumerSecret", value); mOauthConsumerSecret = value }
private var mOauthConsumerSecret: String? = null
var oauthTokenKey: String? var oauthTokenKey: String?
get() { mOauthTokenKey = mOauthTokenKey ?: loginStore.getLoginData("oauthTokenKey", null); return mOauthTokenKey } get() { mOauthTokenKey = mOauthTokenKey ?: loginStore.getLoginData("oauthTokenKey", null); return mOauthTokenKey }
set(value) { loginStore.putLoginData("oauthTokenKey", value); mOauthTokenKey = value } set(value) { loginStore.putLoginData("oauthTokenKey", value); mOauthTokenKey = value }
@ -36,4 +51,14 @@ class DataUsos(
get() { mOauthTokenSecret = mOauthTokenSecret ?: loginStore.getLoginData("oauthTokenSecret", null); return mOauthTokenSecret } get() { mOauthTokenSecret = mOauthTokenSecret ?: loginStore.getLoginData("oauthTokenSecret", null); return mOauthTokenSecret }
set(value) { loginStore.putLoginData("oauthTokenSecret", value); mOauthTokenSecret = value } set(value) { loginStore.putLoginData("oauthTokenSecret", value); mOauthTokenSecret = value }
private var mOauthTokenSecret: String? = null private var mOauthTokenSecret: String? = null
var studentId: String?
get() { mStudentId = mStudentId ?: profile?.getStudentData("studentId", null); return mStudentId }
set(value) { profile?.putStudentData("studentId", value) ?: return; mStudentId = value }
private var mStudentId: String? = null
var studentNumber: String?
get() { mStudentNumber = mStudentNumber ?: profile?.getStudentData("studentNumber", null); return mStudentNumber }
set(value) { profile?.putStudentData("studentNumber", value) ?: return; mStudentNumber = value }
private var mStudentNumber: String? = null
} }

View File

@ -0,0 +1,170 @@
/*
* Copyright (c) Kuba Szczodrzyński 2022-10-13.
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.usos.data
import com.google.gson.JsonArray
import com.google.gson.JsonObject
import im.wangchao.mhttp.AbsCallbackHandler
import im.wangchao.mhttp.Request
import im.wangchao.mhttp.Response
import im.wangchao.mhttp.body.MediaTypeUtils
import im.wangchao.mhttp.callback.JsonArrayCallbackHandler
import im.wangchao.mhttp.callback.JsonCallbackHandler
import im.wangchao.mhttp.callback.TextCallbackHandler
import pl.szczodrzynski.edziennik.data.api.ERROR_REQUEST_FAILURE
import pl.szczodrzynski.edziennik.data.api.SERVER_USER_AGENT
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.DataUsos
import pl.szczodrzynski.edziennik.data.api.models.ApiError
import pl.szczodrzynski.edziennik.ext.currentTimeUnix
import pl.szczodrzynski.edziennik.ext.hmacSHA1
import pl.szczodrzynski.edziennik.ext.toQueryString
import pl.szczodrzynski.edziennik.ext.urlEncode
import pl.szczodrzynski.edziennik.utils.Utils.d
import java.net.HttpURLConnection.*
import java.util.UUID
open class UsosApi(open val data: DataUsos, open val lastSync: Long?) {
companion object {
private const val TAG = "UsosApi"
}
enum class ResponseType {
OBJECT,
ARRAY,
PLAIN,
}
val profileId
get() = data.profile?.id ?: -1
val profile
get() = data.profile
private fun valueToString(value: Any) = when (value) {
is String -> value
is Number -> value.toString()
is List<*> -> listToString(value)
else -> value.toString()
}
private fun listToString(list: List<*>): String {
return list.map {
if (it is Pair<*, *> && it.first is String && it.second is List<*>)
return@map "${it.first}[${listToString(it.second as List<*>)}]"
return@map valueToString(it ?: "")
}.joinToString("|")
}
private fun buildSignature(method: String, url: String, params: Map<String, String>): String {
val query = params.toQueryString()
val signatureString = listOf(
method.uppercase(),
url.urlEncode(),
query.urlEncode(),
).joinToString("&")
val signingKey = listOf(
data.oauthConsumerSecret ?: "",
data.oauthTokenSecret ?: "",
).joinToString("&") { it.urlEncode() }
return signatureString.hmacSHA1(signingKey)
}
fun <T> apiRequest(
tag: String,
service: String,
params: Map<String, Any>,
responseType: ResponseType,
onSuccess: (data: T) -> Unit,
) {
val url = "${data.instanceUrl}/services/$service"
d(tag, "Request: Usos/Api - $url")
val formData = params.mapValues {
valueToString(it.value)
}
val auth = mutableMapOf(
"realm" to url,
"oauth_consumer_key" to (data.oauthConsumerKey ?: ""),
"oauth_nonce" to UUID.randomUUID().toString(),
"oauth_signature_method" to "HMAC-SHA1",
"oauth_timestamp" to currentTimeUnix().toString(),
"oauth_token" to (data.oauthTokenKey ?: ""),
"oauth_version" to "1.0",
)
val signature = buildSignature("POST", url, formData + auth)
auth["oauth_signature"] = signature
val authString = auth.map {
"""${it.key}="${it.value.urlEncode()}""""
}.joinToString(", ")
Request.builder()
.url(url)
.userAgent(SERVER_USER_AGENT)
.addHeader("Authorization", "OAuth $authString")
.post()
.setTextBody(formData.toQueryString(), MediaTypeUtils.APPLICATION_FORM)
.allowErrorCode(HTTP_BAD_REQUEST)
.allowErrorCode(HTTP_UNAUTHORIZED)
.allowErrorCode(HTTP_FORBIDDEN)
.allowErrorCode(HTTP_NOT_FOUND)
.allowErrorCode(HTTP_UNAVAILABLE)
.callback(getCallback(tag, responseType, onSuccess))
.build()
.enqueue()
}
@Suppress("UNCHECKED_CAST")
private fun <T> getCallback(
tag: String,
responseType: ResponseType,
onSuccess: (data: T) -> Unit,
) = when (responseType) {
ResponseType.OBJECT -> object : JsonCallbackHandler() {
override fun onSuccess(data: JsonObject?, response: Response?) {
processResponse(response, data as T, onSuccess)
}
override fun onFailure(response: Response?, throwable: Throwable?) {
processError(tag, response, throwable)
}
}
ResponseType.ARRAY -> object : JsonArrayCallbackHandler() {
override fun onSuccess(data: JsonArray?, response: Response?) {
processResponse(response, data as T, onSuccess)
}
override fun onFailure(response: Response?, throwable: Throwable?) {
processError(tag, response, throwable)
}
}
ResponseType.PLAIN -> object : TextCallbackHandler() {
override fun onSuccess(data: String?, response: Response?) {
processResponse(response, data as T, onSuccess)
}
override fun onFailure(response: Response?, throwable: Throwable?) {
processError(tag, response, throwable)
}
}
}
private fun <T> processResponse(
response: Response?,
data: T?,
onSuccess: (data: T) -> Unit,
) {
}
private fun processError(
tag: String,
response: Response?,
throwable: Throwable?,
) {
data.error(ApiError(tag, ERROR_REQUEST_FAILURE)
.withResponse(response)
.withThrowable(throwable))
}
}

View File

@ -0,0 +1,49 @@
/*
* Copyright (c) Kuba Szczodrzyński 2022-10-13.
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.usos.data
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.api.edziennik.template.data.web.TemplateWebSample
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.DataUsos
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.ENDPOINT_USOS_API_USER
import pl.szczodrzynski.edziennik.utils.Utils.d
class UsosData(val data: DataUsos, val onSuccess: () -> Unit) {
companion object {
private const val TAG = "UsosData"
}
init {
nextEndpoint(onSuccess)
}
private fun nextEndpoint(onSuccess: () -> Unit) {
if (data.targetEndpointIds.isEmpty()) {
onSuccess()
return
}
if (data.cancelled) {
onSuccess()
return
}
val id = data.targetEndpointIds.firstKey()
val lastSync = data.targetEndpointIds.remove(id)
useEndpoint(id, lastSync) { endpointId ->
data.progress(data.progressStep)
nextEndpoint(onSuccess)
}
}
private fun useEndpoint(endpointId: Int, lastSync: Long?, onSuccess: (endpointId: Int) -> Unit) {
d(TAG, "Using endpoint $endpointId. Last sync time = $lastSync")
when (endpointId) {
ENDPOINT_USOS_API_USER -> {
data.startProgress(R.string.edziennik_progress_endpoint_student_info)
TemplateWebSample(data, lastSync, onSuccess)
}
else -> onSuccess(endpointId)
}
}
}

View File

@ -18,6 +18,7 @@ import android.text.style.StyleSpan
import androidx.annotation.PluralsRes import androidx.annotation.PluralsRes
import androidx.annotation.StringRes import androidx.annotation.StringRes
import com.mikepenz.materialdrawer.holder.StringHolder import com.mikepenz.materialdrawer.holder.StringHolder
import java.net.URLEncoder
fun CharSequence?.isNotNullNorEmpty(): Boolean { fun CharSequence?.isNotNullNorEmpty(): Boolean {
return this != null && this.isNotEmpty() return this != null && this.isNotEmpty()
@ -343,3 +344,10 @@ fun Int.toStringHolder() = StringHolder(this)
fun CharSequence.toStringHolder() = StringHolder(this) fun CharSequence.toStringHolder() = StringHolder(this)
fun @receiver:StringRes Int.resolveString(context: Context) = context.getString(this) fun @receiver:StringRes Int.resolveString(context: Context) = context.getString(this)
fun String.urlEncode(): String = URLEncoder.encode(this, "UTF-8").replace("+", "%20")
fun Map<String, String>.toQueryString() = this
.map { it.key.urlEncode() to it.value.urlEncode() }
.sortedBy { it.first }
.joinToString("&") { "${it.first}=${it.second}" }

View File

@ -99,8 +99,8 @@ class UserActionManager(val app: App) {
private fun executeLibrus( private fun executeLibrus(
activity: AppCompatActivity, activity: AppCompatActivity,
profileId: Int?, profileId: Int?,
onSuccess: ((params: Bundle) -> Unit)? = null, onSuccess: ((params: Bundle) -> Unit)?,
onFailure: (() -> Unit)? = null, onFailure: (() -> Unit)?,
) { ) {
if (profileId == null) if (profileId == null)
return return
@ -125,4 +125,16 @@ class UserActionManager(val app: App) {
onFailure = onFailure onFailure = onFailure
).show() ).show()
} }
private fun executeOauth(
activity: AppCompatActivity,
profileId: Int?,
params: Bundle?,
onSuccess: ((params: Bundle) -> Unit)?,
onFailure: (() -> Unit)?,
) {
if (profileId == null || params == null)
return
}
} }