last commit

This commit is contained in:
2026-09-23 01:15:04 +03:00
parent 655e8424e5
commit 7c876dc10b
103 changed files with 4213 additions and 178 deletions
+1
View File
@@ -0,0 +1 @@
/build
+58
View File
@@ -0,0 +1,58 @@
plugins {
alias(libs.plugins.androidLibrary)
alias(libs.plugins.kotlinAndroid)
alias(libs.plugins.ksp)
}
android {
namespace = "ru.megboyzz.data"
compileSdk = 34
defaultConfig {
minSdk = 24
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
}
dependencies {
ksp(libs.room.compiler)
ksp(libs.dagger.compiler)
implementation(libs.room.ktx)
implementation(libs.room.runtime)
implementation(libs.dagger)
implementation(libs.retrofit)
implementation(libs.retrofit.gson)
implementation(libs.jsoup)
implementation(libs.core.ktx)
implementation(libs.appcompat)
implementation(libs.material)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.test.ext.junit)
androidTestImplementation(libs.espresso.core)
implementation(project(":domain"))
}
View File
+21
View File
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
@@ -0,0 +1,24 @@
package ru.megboyzz.data
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("ru.megboyzz.data.test", appContext.packageName)
}
}
+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>
@@ -0,0 +1,153 @@
package ru.megboyzz.dnevnik.authorization
import android.util.Log
import com.google.gson.Gson
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import org.jsoup.Connection
import org.jsoup.HttpStatusException
import org.jsoup.Jsoup
import ru.megboyzz.dnevnik.db.dao.CredentialsDao
import ru.megboyzz.dnevnik.db.dao.GlobalUserContextDao
import ru.megboyzz.dnevnik.di.DaggerAppComponent
import ru.megboyzz.dnevnik.entities.context.marks.init.state.MarksContext
import ru.megboyzz.dnevnik.entities.db.Credentials
import ru.megboyzz.dnevnik.entities.db.GlobalUserContext
import ru.megboyzz.dnevnik.service.ContextService
import java.net.URL
import javax.inject.Inject
class AuthorizationManager @Inject constructor(
val contextService: ContextService,
val credentialsDao: CredentialsDao,
val globalUserContextDao: GlobalUserContextDao
) {
private val returnUrl =
"https://login.dnevnik.ru/oauth2?response_type=token&client_id=bb97b3e445a340b9b9cab4b9ea0dbd6f&scope=CommonInfo,ContactInfo,FriendsAndRelatives,EducationalInfo"
private val baseUrl = "https://login.dnevnik.ru/login/"
private val marksState = "https://dnevnik.ru/marks"
private var credentials = Credentials(token = "", cookies = mapOf(), rememberMe = false)
private val gson = Gson()
fun login(
login: String,
password: String,
rememberMe: Boolean
): AuthorizationStatus {
runCatching<Connection.Response> {
Jsoup
.connect(baseUrl)
.method(Connection.Method.POST)
.data("ReturnUrl", returnUrl)
.data("login", login)
.data("password", password)
.followRedirects(true)
.execute()
}.onFailure {
if(it is HttpStatusException && it.statusCode == 405)
return AuthorizationStatus.MAINTAIN
return AuthorizationStatus.NO_INTERNET
}.onSuccess { response ->
if(response.statusCode() != 200)
return AuthorizationStatus.MAINTAIN
val url = response.url()
if(url.getResult() != "success")
return AuthorizationStatus.WRONG_CREDENTIALS
//Безопасная зона
val cookies = response.cookies()
credentials = Credentials(
token = url.getToken(),
cookies = cookies,
rememberMe = rememberMe
)
dbUpdate()
kotlin.runCatching {
Jsoup
.connect(marksState)
.method(Connection.Method.GET)
.cookies(cookies)
.ignoreContentType(true)
.followRedirects(true)
.execute()
}.onFailure {
Log.e("Auth", it.message.toString())
return AuthorizationStatus.NO_INTERNET
}.onSuccess { response1 ->
val htmlByLines = response1.body().split("\n")
val contextLine = htmlByLines.find { it.contains("window.__MARKS__INITIAL__STATE__") }
val marksContextStr =
contextLine?.substring(contextLine.indexOf("{"), contextLine.length - 2)
val marksContext = gson.fromJson(marksContextStr, MarksContext::class.java)
val context = contextService.getContext(credentials.token, marksContext.context.userId).execute()
if(context.isSuccessful){
val body = context.body() ?: return AuthorizationStatus.UNKNOWN_ERROR
globalUserContextDao.setGlobalUserContext(GlobalUserContext(
apiUserContext = body,
marksInitStateUserContext = marksContext
))
} else return AuthorizationStatus.UNKNOWN_ERROR
}
}
return AuthorizationStatus.LOGGED
}
fun checkCredentials(): AuthorizationStatus{
val credentialsIsExists = credentialsDao.credentialsIsExists()
return if(credentialsIsExists)
AuthorizationStatus.LOGGED
else
AuthorizationStatus.NOTHING
}
fun dbUpdate(){
credentialsDao.setCredentials(credentials)
}
fun deleteCredentials(){
credentialsDao.deleteCredentials()
}
fun URL.getResult(): String{
val httpUrl = this.toString().toHttpUrlOrNull() ?: return ""
return httpUrl.queryParameter("result") ?: ""
}
fun URL.getToken(): String{
val tokenRegex = "access_token=([A-Za-z0-9]+)"
val regex = Regex(tokenRegex)
val find = regex.find(this.toString())
if(find != null)
return find.groupValues[1]
return ""
}
}
@@ -0,0 +1,15 @@
package ru.megboyzz.dnevnik.authorization
enum class AuthorizationStatus {
LOGGED,
LOGGING,
WRONG_CREDENTIALS,
NO_INTERNET,
MAINTAIN,
CREDENTIALS_LOADING,
NOTHING,
UNKNOWN_ERROR,
LOGOUT
}
@@ -0,0 +1,19 @@
package ru.megboyzz.dnevnik.db
import androidx.room.Database
import androidx.room.RoomDatabase
import ru.megboyzz.dnevnik.db.dao.CredentialsDao
import ru.megboyzz.dnevnik.db.dao.GlobalUserContextDao
import ru.megboyzz.dnevnik.entities.db.Credentials
import ru.megboyzz.dnevnik.entities.db.GlobalUserContext
@Database(
entities = [Credentials::class, GlobalUserContext::class],
version = 1
)
abstract class AppDataBase: RoomDatabase() {
abstract fun credentialsDao(): CredentialsDao
abstract fun globalUserContextDao(): GlobalUserContextDao
}
@@ -0,0 +1,18 @@
package ru.megboyzz.dnevnik.db.converters
import androidx.room.TypeConverter
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
class CookiesConverter {
private val gson = Gson()
private val mapType = object : TypeToken<Map<String, String>>() {}.type
@TypeConverter
fun fromMap(cookies: Map<String, String>): String = gson.toJson(cookies)
@TypeConverter
fun toMap(savedCookies: String): Map<String, String> = gson.fromJson(savedCookies, mapType)
}
@@ -0,0 +1,24 @@
package ru.megboyzz.dnevnik.db.converters
import androidx.room.TypeConverter
import com.google.gson.Gson
import ru.megboyzz.dnevnik.entities.context.marks.init.state.MarksContext
import ru.megboyzz.dnevnik.entities.response.UserContext
class DataStructureConverter {
val gson = Gson()
@TypeConverter
fun fromUserContext(userContext: UserContext): String = gson.toJson(userContext)
@TypeConverter
fun toUserContext(savedUserContext: String): UserContext = gson.fromJson(savedUserContext, UserContext::class.java)
@TypeConverter
fun fromMarksContext(marksContext: MarksContext): String = gson.toJson(marksContext)
@TypeConverter
fun toMarksContext(savedMarksContext: String): MarksContext = gson.fromJson(savedMarksContext, MarksContext::class.java)
}
@@ -0,0 +1,24 @@
package ru.megboyzz.dnevnik.db.dao
import androidx.lifecycle.LiveData
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import ru.megboyzz.dnevnik.entities.db.Credentials
@Dao
interface CredentialsDao {
@Query("SELECT EXISTS(SELECT * FROM Credentials WHERE id = 0)")
fun credentialsIsExists(): Boolean
@Query("SELECT * FROM Credentials WHERE id = 0")
fun getCredentials(): Credentials
@Query("DELETE FROM Credentials WHERE id = 0")
fun deleteCredentials()
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun setCredentials(profile: Credentials)
}
@@ -0,0 +1,24 @@
package ru.megboyzz.dnevnik.db.dao
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import ru.megboyzz.dnevnik.entities.db.GlobalUserContext
@Dao
interface GlobalUserContextDao {
@Query("SELECT EXISTS(SELECT * FROM GlobalUserContext WHERE id = 0)")
fun globalUserContextIsExists(): Boolean
@Query("SELECT * FROM GlobalUserContext WHERE id = 0")
fun getGlobalUserContext(): GlobalUserContext
@Query("DELETE FROM GlobalUserContext WHERE id = 0")
fun deleteGlobalUserContext()
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun setGlobalUserContext(profile: GlobalUserContext)
}
@@ -0,0 +1,42 @@
package ru.megboyzz.dnevnik.di
import android.app.Activity
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.ViewModel
import dagger.BindsInstance
import dagger.Component
import ru.megboyzz.dnevnik.MainActivity
import ru.megboyzz.dnevnik.authorization.AuthorizationManager
import ru.megboyzz.dnevnik.db.dao.CredentialsDao
import ru.megboyzz.dnevnik.db.dao.GlobalUserContextDao
import ru.megboyzz.dnevnik.di.modules.AppModule
import ru.megboyzz.dnevnik.entities.db.Credentials
import ru.megboyzz.dnevnik.viewmodel.AuthorizationViewModel
import ru.megboyzz.dnevnik.viewmodel.ProfileViewModel
import ru.megboyzz.dnevnik.viewmodel.subscreen.LastMarksViewModel
import javax.inject.Singleton
@Singleton
@Component(modules = [AppModule::class])
interface AppComponent {
val authViewModel: AuthorizationViewModel
val profileViewModel: ProfileViewModel
val lastMarksViewModel: LastMarksViewModel
val credentialsDao: CredentialsDao
val globalUserContextDao: GlobalUserContextDao
fun inject(viewModel: ProfileViewModel)
fun inject(activity: MainActivity)
@Component.Builder
interface Builder {
fun appModule(appModule: AppModule): Builder // add this
fun build(): AppComponent
}
}
@@ -0,0 +1,42 @@
package ru.megboyzz.dnevnik.di.modules
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import dagger.Module
import dagger.Provides
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.create
import ru.megboyzz.dnevnik.service.ContextService
import ru.megboyzz.dnevnik.service.MarksService
import ru.megboyzz.dnevnik.util.gson.DateDeserializer
import ru.megboyzz.dnevnik.util.gson.DateSerializer
import java.util.*
@Module
class ApiModule {
private val apiUrl = "https://api.dnevnik.ru"
@Provides
fun provideApiRetrofit(gson: Gson): Retrofit{
return Retrofit
.Builder()
.baseUrl(apiUrl)
.addConverterFactory(GsonConverterFactory.create(gson))
.build()
}
@Provides
fun provideGson(): Gson = GsonBuilder()
//.registerTypeAdapter(Date::class.java, DateSerializer())
.registerTypeAdapter(Date::class.java, DateDeserializer())
.create()
@Provides
fun provideContextService(retrofit: Retrofit): ContextService = retrofit.create()
@Provides
fun provideMarksService(retrofit: Retrofit): MarksService = retrofit.create()
}
@@ -0,0 +1,20 @@
package ru.megboyzz.dnevnik.di.modules
import android.app.Application
import android.content.Context
import dagger.Module
import dagger.Provides
import javax.inject.Singleton
@Module(includes = [ApiModule::class, DatabaseModule::class, AuthModule::class])
class AppModule(private val application: Application) {
@Provides
@Singleton
fun provideContext(): Context = this.application.applicationContext
@Provides
@Singleton
fun provideApplication(): Application = application
}
@@ -0,0 +1,15 @@
package ru.megboyzz.dnevnik.di.modules
import dagger.Module
import dagger.Provides
import ru.megboyzz.dnevnik.authorization.AuthorizationManager
import ru.megboyzz.dnevnik.db.dao.CredentialsDao
import ru.megboyzz.dnevnik.db.dao.GlobalUserContextDao
import ru.megboyzz.dnevnik.service.ContextService
@Module
class AuthModule {
}
@@ -0,0 +1,33 @@
package ru.megboyzz.dnevnik.di.modules
import android.content.Context
import androidx.room.Room
import dagger.Module
import dagger.Provides
import kotlinx.coroutines.coroutineScope
import ru.megboyzz.dnevnik.db.AppDataBase
import ru.megboyzz.dnevnik.db.dao.CredentialsDao
import ru.megboyzz.dnevnik.db.dao.GlobalUserContextDao
import ru.megboyzz.dnevnik.entities.db.Credentials
import javax.inject.Singleton
import kotlin.coroutines.coroutineContext
@Module
class DatabaseModule {
@Singleton
@Provides
fun provideDatabase(context: Context): AppDataBase {
return Room
.databaseBuilder(context, AppDataBase::class.java, "database")
.build()
}
@Provides
fun provideCredentialsDao(dataBase: AppDataBase): CredentialsDao = dataBase.credentialsDao()
@Provides
fun provideGlobalUserContextDao(dataBase: AppDataBase): GlobalUserContextDao = dataBase.globalUserContextDao()
}
@@ -0,0 +1,19 @@
package ru.megboyzz.dnevnik.service
import retrofit2.Call
import retrofit2.http.*
import ru.megboyzz.dnevnik.entities.response.UserContext
import ru.megboyzz.dnevnik.entities.teachers.Teacher
interface ContextService{
@GET("/mobile/v4/users/{personId}/context")
fun getContext(@Header("Access-Token") accessToken: String, @Path("personId") personId: String): Call<UserContext>
@GET("/v1/edu-groups/{groupId}/teachers")
fun getTeachers(
@Header("Access-Token") accessToken: String,
@Path("groupId") groupId: Long
): Call<List<Teacher>>
}
@@ -0,0 +1,22 @@
package ru.megboyzz.dnevnik.service
import retrofit2.Call
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.Path
import ru.megboyzz.dnevnik.entities.marks.RecentMarks
//Сервис для последних оценок
interface MarksService {
@GET("/v2/persons/{personId}/group/{groupId}/recentmarks")
fun getRecentMarks(
@Header("Access-Token") accessToken: String,
@Path("personId") personId: Long,
@Path("groupId") groupId: Long
): Call<RecentMarks>
}
@@ -0,0 +1,15 @@
package ru.megboyzz.dnevnik.util.gson
import com.google.gson.JsonDeserializationContext
import com.google.gson.JsonDeserializer
import com.google.gson.JsonElement
import com.google.gson.JsonParseException
import java.lang.reflect.Type
import java.util.*
class DateDeserializer : JsonDeserializer<Date> {
override fun deserialize(json: JsonElement, type: Type, context: JsonDeserializationContext): Date {
val date = json.asString
return Date()
}
}
@@ -0,0 +1,15 @@
package ru.megboyzz.dnevnik.util.gson
import com.google.gson.JsonElement
import com.google.gson.JsonPrimitive
import com.google.gson.JsonSerializationContext
import com.google.gson.JsonSerializer
import java.lang.reflect.Type
import java.util.*
class DateSerializer : JsonSerializer<Date> {
override fun serialize(date: Date?, typeOfSrc: Type?, context: JsonSerializationContext?): JsonElement {
val formattedDate = date?.time?.let { "/Date($it)/" }
return JsonPrimitive(formattedDate)
}
}
@@ -0,0 +1,17 @@
package ru.megboyzz.data
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}