Add some register methods, pojos and more

This commit is contained in:
Mikołaj Pich 2020-03-22 23:44:48 +01:00
parent f14abea44c
commit 9f172c1056
16 changed files with 564 additions and 9 deletions

View file

@ -1,6 +1,10 @@
package io.github.wulkanowy.sdk.hebe
import com.google.gson.annotations.SerializedName
import org.threeten.bp.LocalDateTime.now
import org.threeten.bp.ZoneOffset.UTC
import org.threeten.bp.format.DateTimeFormatter.ofPattern
import java.util.UUID
data class ApiRequest<T>(
@ -23,11 +27,11 @@ data class ApiRequest<T>(
val firebaseToken: String,
@SerializedName("RequestId")
val requestId: String,
val requestId: String = UUID.randomUUID().toString(),
@SerializedName("Timestamp")
val timestamp: Long,
val timestamp: Long = now().toEpochSecond(UTC),
@SerializedName("TimestampFormatted")
val timestampFormatted: String
val timestampFormatted: String = now().format(ofPattern("yyyy-MM-dd hh:mm:ss"))
)

View file

@ -1,8 +1,63 @@
package io.github.wulkanowy.sdk.hebe
import io.github.wulkanowy.sdk.hebe.register.RegisterResponse
import io.github.wulkanowy.sdk.hebe.register.StudentInfo
import io.github.wulkanowy.sdk.hebe.repository.RepositoryManager
import io.reactivex.Single
import okhttp3.Interceptor
import okhttp3.logging.HttpLoggingInterceptor
class Hebe {
fun register(token: String, pin: String, symbol: String) {
private val resettableManager = resettableManager()
var logLevel = HttpLoggingInterceptor.Level.BASIC
set(value) {
field = value
resettableManager.reset()
}
var privateKey = ""
set(value) {
field = value
resettableManager.reset()
}
var baseUrl = ""
set(value) {
field = value
resettableManager.reset()
}
var schoolSymbol = ""
set(value) {
field = value
resettableManager.reset()
}
var deviceModel = ""
set(value) {
field = value
resettableManager.reset()
}
private val interceptors: MutableList<Pair<Interceptor, Boolean>> = mutableListOf()
private val serviceManager by resettableLazy(resettableManager) { RepositoryManager(logLevel, privateKey, interceptors, baseUrl, schoolSymbol) }
private val routes by resettableLazy(resettableManager) { serviceManager.getRoutesRepository() }
fun register(privateKey: String, certificateId: String, token: String, pin: String, symbol: String): Single<ApiResponse<RegisterResponse>> {
return routes.getRouteByToken(token).flatMap { baseUrl ->
serviceManager
.getRegisterRepository(baseUrl, symbol)
.registerDevice(privateKey, certificateId, deviceModel, pin, token)
}
}
fun getStudents(url: String, symbol: String): Single<List<StudentInfo>> {
return serviceManager
.getRegisterRepository(url, symbol)
.getStudentInfo()
}
}

View file

@ -0,0 +1,55 @@
package io.github.wulkanowy.sdk.hebe
import java.util.LinkedList
import kotlin.reflect.KProperty
/**
* see https://stackoverflow.com/a/35757638/6695449
*/
class ResettableLazyManager {
// we synchronize to make sure the timing of a reset() call and new inits do not collide
val managedDelegates = LinkedList<Resettable>()
fun register(managed: Resettable) {
synchronized(managedDelegates) {
managedDelegates.add(managed)
}
}
fun reset() {
synchronized(managedDelegates) {
managedDelegates.forEach { it.reset() }
managedDelegates.clear()
}
}
}
interface Resettable {
fun reset()
}
class ResettableLazy<PROPTYPE>(val manager: ResettableLazyManager, val init: () -> PROPTYPE) : Resettable {
@Volatile
var lazyHolder = makeInitBlock()
operator fun getValue(thisRef: Any?, property: KProperty<*>): PROPTYPE {
return lazyHolder.value
}
override fun reset() {
lazyHolder = makeInitBlock()
}
fun makeInitBlock(): Lazy<PROPTYPE> {
return lazy {
manager.register(this)
init()
}
}
}
fun <PROPTYPE> resettableLazy(manager: ResettableLazyManager, init: () -> PROPTYPE): ResettableLazy<PROPTYPE> {
return ResettableLazy(manager, init)
}
fun resettableManager(): ResettableLazyManager = ResettableLazyManager()

View file

@ -0,0 +1,5 @@
package io.github.wulkanowy.sdk.hebe.exception
import java.io.IOException
class InvalidTokenException(message: String) : IOException(message)

View file

@ -0,0 +1,5 @@
package io.github.wulkanowy.sdk.hebe.exception
import java.io.IOException
class UnknownTokenException(message: String) : IOException(message)

View file

@ -0,0 +1,11 @@
package io.github.wulkanowy.sdk.hebe.interceptor
import okhttp3.Interceptor
import okhttp3.Response
class ErrorInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
return chain.proceed(chain.request())
}
}

View file

@ -0,0 +1,11 @@
package io.github.wulkanowy.sdk.hebe.interceptor
import okhttp3.Interceptor
import okhttp3.Response
class SignInterceptor(private val privateKey: String) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
return chain.proceed(chain.request())
}
}

View file

@ -1,6 +1,7 @@
package io.github.wulkanowy.sdk.hebe.register
import com.google.gson.annotations.SerializedName
import java.util.UUID
data class RegisterRequest(
@ -26,5 +27,5 @@ data class RegisterRequest(
val securityToken: String,
@SerializedName("SelfIdentifier")
val selfIdentifier: String
val selfIdentifier: String = UUID.randomUUID().toString()
)

View file

@ -0,0 +1,265 @@
package io.github.wulkanowy.sdk.hebe.register
import com.google.gson.annotations.SerializedName
class StudentInfo(
@SerializedName("Capabilities")
val capabilities: List<String>,
@SerializedName("ClassDisplay")
val classDisplay: String,
@SerializedName("ConstituentUnit")
val constituentUnit: ConstituentUnit,
@SerializedName("Educators")
val educators: List<Educator>,
@SerializedName("FullSync")
val fullSync: Boolean,
@SerializedName("InfoDisplay")
val infoDisplay: String,
@SerializedName("Journal")
val journal: Journal,
@SerializedName("Login")
val login: Login,
@SerializedName("Partition")
val partition: String,
@SerializedName("Periods")
val periods: List<Period>,
@SerializedName("Pupil")
val pupil: Pupil,
@SerializedName("SenderEntry")
val senderEntry: SenderEntry,
@SerializedName("TopLevelPartition")
val topLevelPartition: String,
@SerializedName("Unit")
val unit: Unit
) {
data class ConstituentUnit(
@SerializedName("Address")
val address: String,
@SerializedName("Id")
val id: Int,
@SerializedName("Name")
val name: String,
@SerializedName("Patron")
val patron: String,
@SerializedName("SchoolTopic")
val schoolTopic: String,
@SerializedName("Short")
val short: String
)
data class Educator(
@SerializedName("Id")
val id: Int,
@SerializedName("Initials")
val initials: String,
@SerializedName("LoginId")
val loginId: Int,
@SerializedName("Name")
val name: String,
@SerializedName("Surname")
val surname: String,
@SerializedName("Roles")
val roles: List<Role>
) {
data class Role(
@SerializedName("Address")
val address: String,
@SerializedName("AddressHash")
val addressHash: String,
@SerializedName("ClassSymbol")
val classSymbol: String,
@SerializedName("ConstituentUnitSymbol")
val constituentUnitSymbol: String,
@SerializedName("Initials")
val initials: String,
@SerializedName("Name")
val name: String,
@SerializedName("RoleName")
val roleName: String,
@SerializedName("RoleOrder")
val roleOrder: Int,
@SerializedName("Surname")
val surname: String,
@SerializedName("UnitSymbol")
val unitSymbol: String?
)
}
data class Journal(
@SerializedName("Id")
val id: Int,
@SerializedName("YearStart")
val yearStart: PeriodDate,
@SerializedName("YearEnd")
val yearEnd: PeriodDate
)
data class PeriodDate(
@SerializedName("Date")
val date: String,
@SerializedName("DateDisplay")
val dateDisplay: String,
@SerializedName("Time")
val time: String,
@SerializedName("Timestamp")
val timestamp: Long
)
data class Login(
@SerializedName("DisplayName")
val displayName: String,
@SerializedName("FirstName")
val firstName: String,
@SerializedName("Id")
val id: Int,
@SerializedName("LoginRole")
val loginRole: String,
@SerializedName("SecondName")
val secondName: String,
@SerializedName("Surname")
val surname: String,
@SerializedName("Value")
val value: String
)
data class Period(
@SerializedName("Current")
val current: Boolean,
@SerializedName("End")
val end: PeriodDate,
@SerializedName("Id")
val id: Int,
@SerializedName("Last")
val last: Boolean,
@SerializedName("Level")
val level: Int,
@SerializedName("Number")
val number: Int,
@SerializedName("Start")
val start: PeriodDate
)
data class Pupil(
@SerializedName("FirstName")
val firstName: String,
@SerializedName("Id")
val id: Int,
@SerializedName("LoginId")
val loginId: Int,
@SerializedName("LoginValue")
val loginValue: String,
@SerializedName("SecondName")
val secondName: String,
@SerializedName("Sex")
val sex: Boolean,
@SerializedName("Surname")
val surname: String
)
data class SenderEntry(
@SerializedName("Address")
val address: String,
@SerializedName("AddressHash")
val addressHash: String,
@SerializedName("Initials")
val initials: String,
@SerializedName("LoginId")
val loginId: Int
)
data class Unit(
@SerializedName("Address")
val address: String,
@SerializedName("DisplayName")
val displayName: String,
@SerializedName("Id")
val id: Int,
@SerializedName("Name")
val name: String,
@SerializedName("Patron")
val patron: String,
@SerializedName("RestURL")
val restUrl: String,
@SerializedName("Short")
val short: String,
@SerializedName("Symbol")
val symbol: String
)
}

View file

@ -1,4 +1,30 @@
package io.github.wulkanowy.sdk.hebe.repository
class RegisterRepository {
import io.github.wulkanowy.sdk.hebe.ApiRequest
import io.github.wulkanowy.sdk.hebe.ApiResponse
import io.github.wulkanowy.sdk.hebe.register.RegisterRequest
import io.github.wulkanowy.sdk.hebe.register.RegisterResponse
import io.github.wulkanowy.sdk.hebe.register.StudentInfo
import io.github.wulkanowy.sdk.hebe.service.RegisterService
import io.reactivex.Single
class RegisterRepository(private val service: RegisterService) {
fun registerDevice(privateKey: String, certificateId: String, deviceModel: String, pin: String, token: String): Single<ApiResponse<RegisterResponse>> {
return service.registerDevice(ApiRequest(
certificateId = certificateId,
firebaseToken = "",
envelope = RegisterRequest(
certificate = privateKey,
certificateThumbprint = certificateId,
deviceModel = deviceModel,
pin = pin,
securityToken = token
)
))
}
fun getStudentInfo(): Single<List<StudentInfo>> {
return service.getStudentsInfo().map { requireNotNull(it.envelope) }
}
}

View file

@ -1,4 +1,50 @@
package io.github.wulkanowy.sdk.hebe.repository
class RepositoryManager {
import io.github.wulkanowy.sdk.hebe.interceptor.ErrorInterceptor
import io.github.wulkanowy.sdk.hebe.interceptor.SignInterceptor
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.converter.scalars.ScalarsConverterFactory
import retrofit2.create
class RepositoryManager(
private val logLevel: HttpLoggingInterceptor.Level,
private val privateKey: String,
private val interceptors: MutableList<Pair<Interceptor, Boolean>>,
private val baseUrl: String,
private val schoolSymbol: String
) {
fun getRoutesRepository(): RoutingRulesRepository {
return RoutingRulesRepository(getRetrofitBuilder(interceptors).baseUrl("http://komponenty.vulcan.net.pl").build().create())
}
fun getRegisterRepository(baseUrl: String, symbol: String) = getRegisterRepository("${baseUrl.removeSuffix("/")}/$symbol")
fun getRegisterRepository(baseUrl: String): RegisterRepository {
return RegisterRepository(getRetrofitBuilder(interceptors).baseUrl("${baseUrl.removeSuffix("/")}/api/mobile/register/").build().create())
}
private fun getRetrofitBuilder(interceptors: MutableList<Pair<Interceptor, Boolean>>): Retrofit.Builder {
return Retrofit.Builder()
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.addConverterFactory(ScalarsConverterFactory.create())
.addConverterFactory(GsonConverterFactory.create())
.client(OkHttpClient().newBuilder()
.addInterceptor(HttpLoggingInterceptor().setLevel(logLevel))
.addInterceptor(ErrorInterceptor())
.addInterceptor(SignInterceptor(privateKey))
.apply {
interceptors.forEach {
if (it.second) addNetworkInterceptor(it.first)
else addInterceptor(it.first)
}
}
.build()
)
}
}

View file

@ -0,0 +1,24 @@
package io.github.wulkanowy.sdk.hebe.repository
import io.github.wulkanowy.sdk.hebe.exception.InvalidTokenException
import io.github.wulkanowy.sdk.hebe.exception.UnknownTokenException
import io.github.wulkanowy.sdk.hebe.service.RoutingRulesService
import io.reactivex.Single
class RoutingRulesRepository(private val api: RoutingRulesService) {
fun getRouteByToken(token: String): Single<String> {
if (token.length < 4) return Single.error<String>(InvalidTokenException("Token '$token' is too short"))
val tokenSymbol = token.substring(0..2)
if ("FK1" == tokenSymbol) return Single.just("https://api.fakelog.cf")
return api.getRoutingRules().map { routes ->
routes.split("\r?\n".toRegex())
.singleOrNull { tokenSymbol == it.substringBefore(",") }
?.substringAfter(",")
?: throw UnknownTokenException("This token: '$token' is unsupported")
}
}
}

View file

@ -4,9 +4,16 @@ import io.github.wulkanowy.sdk.hebe.ApiRequest
import io.github.wulkanowy.sdk.hebe.ApiResponse
import io.github.wulkanowy.sdk.hebe.register.RegisterRequest
import io.github.wulkanowy.sdk.hebe.register.RegisterResponse
import io.github.wulkanowy.sdk.hebe.register.StudentInfo
import io.reactivex.Single
import retrofit2.http.GET
import retrofit2.http.POST
interface RegisterService {
@POST("new")
fun registerDevice(request: ApiRequest<RegisterRequest>): Single<ApiResponse<RegisterResponse>>
@GET("hebe")
fun getStudentsInfo(): Single<ApiResponse<List<StudentInfo>>>
}

View file

@ -0,0 +1,10 @@
package io.github.wulkanowy.sdk.hebe.service
import io.reactivex.Single
import retrofit2.http.GET
interface RoutingRulesService {
@GET("/UonetPlusMobile/RoutingRules.txt")
fun getRoutingRules(): Single<String>
}

View file

@ -184,8 +184,13 @@ class Sdk {
.map { it.mapStudents(symbol) }
}
fun getStudentsFromHebe(token: String, pin: String, symbol: String) {
return hebe.register(token, pin, symbol)
fun getStudentsFromHebe(token: String, pin: String, symbol: String): Single<List<Student>> {
val privateKey = "" // TODO
val certificateId = "" // TODO
return hebe.register(privateKey, certificateId, token, pin, symbol)
.flatMap { hebe.getStudents(it.envelope!!.restUrl, symbol) }
.map { it.mapHebeStudents(certificateId, privateKey) }
}
fun getStudentsFromScrapper(email: String, password: String, scrapperBaseUrl: String, symbol: String = "Default"): Single<List<Student>> {

View file

@ -1,6 +1,7 @@
package io.github.wulkanowy.sdk.mapper
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.sdk.hebe.register.StudentInfo
import io.github.wulkanowy.sdk.pojo.Student
import io.github.wulkanowy.sdk.scrapper.register.Student as ScrapperStudent
import io.github.wulkanowy.sdk.mobile.register.Student as ApiStudent
@ -52,3 +53,27 @@ fun List<ScrapperStudent>.mapStudents(): List<Student> {
)
}
}
fun List<StudentInfo>.mapHebeStudents(certificateKey: String, privateKey: String): List<Student> {
return map {
Student(
email = it.pupil.loginValue,
isParent = it.login.loginRole != "Uczen",
className = it.classDisplay,
classId = -1,
studentId = it.pupil.id,
userLoginId = it.pupil.loginId,
symbol = it.topLevelPartition,
loginType = Sdk.ScrapperLoginType.STANDARD,
schoolName = it.constituentUnit.name,
schoolShortName = it.constituentUnit.short,
schoolSymbol = it.unit.symbol,
studentName = it.pupil.let { pupil -> "${pupil.firstName} ${pupil.surname}" },
loginMode = Sdk.Mode.HEBE,
scrapperBaseUrl = "",
mobileBaseUrl = it.unit.restUrl,
certificateKey = certificateKey,
privateKey = privateKey
)
}
}