Initial commit

This commit is contained in:
Finnley Somdahl 2023-10-17 18:42:43 -05:00
commit 21bfbfb139
520 changed files with 47819 additions and 0 deletions

View file

@ -0,0 +1,52 @@
package ani.dantotsu.connections.mal
import android.net.Uri
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import ani.dantotsu.*
import ani.dantotsu.connections.mal.MAL.clientId
import ani.dantotsu.connections.mal.MAL.saveResponse
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class Login : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
try {
val data: Uri = intent?.data
?: throw Exception(getString(R.string.mal_login_uri_not_found))
val codeChallenge: String = loadData("malCodeChallenge", this)
?: throw Exception(getString(R.string.mal_login_code_challenge_not_found))
val code = data.getQueryParameter("code")
?: throw Exception(getString(R.string.mal_login_code_not_present))
snackString(getString(R.string.logging_in_mal))
lifecycleScope.launch(Dispatchers.IO) {
tryWithSuspend(true) {
val res = client.post(
"https://myanimelist.net/v1/oauth2/token",
data = mapOf(
"client_id" to clientId,
"code" to code,
"code_verifier" to codeChallenge,
"grant_type" to "authorization_code"
)
).parsed<MAL.ResponseToken>()
saveResponse(res)
MAL.token = res.accessToken
snackString(getString(R.string.getting_user_data))
MAL.query.getUserData()
launch(Dispatchers.Main) {
startMainActivity(this@Login)
}
}
}
}
catch (e:Exception){
logError(e,snackbar = false)
startMainActivity(this)
}
}
}

View file

@ -0,0 +1,99 @@
package ani.dantotsu.connections.mal
import android.content.ActivityNotFoundException
import android.content.Context
import android.net.Uri
import android.util.Base64
import androidx.browser.customtabs.CustomTabsIntent
import androidx.fragment.app.FragmentActivity
import ani.dantotsu.*
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import java.io.File
import java.security.SecureRandom
object MAL {
val query: MALQueries = MALQueries()
const val clientId = "86b35cf02205a0303da3aaea1c9e33f3"
var username: String? = null
var avatar: String? = null
var token: String? = null
var userid: Int? = null
fun loginIntent(context: Context) {
val codeVerifierBytes = ByteArray(96)
SecureRandom().nextBytes(codeVerifierBytes)
val codeChallenge = Base64.encodeToString(codeVerifierBytes, Base64.DEFAULT).trimEnd('=')
.replace("+", "-")
.replace("/", "_")
.replace("\n", "")
saveData("malCodeChallenge", codeChallenge, context)
val request =
"https://myanimelist.net/v1/oauth2/authorize?response_type=code&client_id=$clientId&code_challenge=$codeChallenge"
try {
CustomTabsIntent.Builder().build().launchUrl(
context,
Uri.parse(request)
)
} catch (e: ActivityNotFoundException) {
openLinkInBrowser(request)
}
}
private const val MAL_TOKEN = "malToken"
private suspend fun refreshToken(): ResponseToken? {
return tryWithSuspend {
val token = loadData<ResponseToken>(MAL_TOKEN)
?: throw Exception(currContext()?.getString(R.string.refresh_token_load_failed))
val res = client.post(
"https://myanimelist.net/v1/oauth2/token",
data = mapOf(
"client_id" to clientId,
"grant_type" to "refresh_token",
"refresh_token" to token.refreshToken
)
).parsed<ResponseToken>()
saveResponse(res)
return@tryWithSuspend res
}
}
suspend fun getSavedToken(context: FragmentActivity): Boolean {
return tryWithSuspend(false) {
var res: ResponseToken = loadData(MAL_TOKEN, context)
?: return@tryWithSuspend false
if (System.currentTimeMillis() > res.expiresIn)
res = refreshToken()
?: throw Exception(currContext()?.getString(R.string.refreshing_token_failed))
token = res.accessToken
return@tryWithSuspend true
} ?: false
}
fun removeSavedToken(context: Context) {
token = null
username = null
userid = null
avatar = null
if (MAL_TOKEN in context.fileList()) {
File(context.filesDir, MAL_TOKEN).delete()
}
}
fun saveResponse(res: ResponseToken) {
res.expiresIn += System.currentTimeMillis()
saveData(MAL_TOKEN, res)
}
@Serializable
data class ResponseToken(
@SerialName("token_type") val tokenType: String,
@SerialName("expires_in") var expiresIn: Long,
@SerialName("access_token") val accessToken: String,
@SerialName("refresh_token") val refreshToken: String,
): java.io.Serializable
}

View file

@ -0,0 +1,90 @@
package ani.dantotsu.connections.mal
import ani.dantotsu.connections.anilist.api.FuzzyDate
import ani.dantotsu.client
import ani.dantotsu.tryWithSuspend
import kotlinx.serialization.Serializable
class MALQueries {
private val apiUrl = "https://api.myanimelist.net/v2"
private val authHeader: Map<String, String>?
get() {
return mapOf("Authorization" to "Bearer ${MAL.token ?: return null}")
}
@Serializable
data class MalUser(
val id: Int,
val name: String,
val picture: String?,
)
suspend fun getUserData(): Boolean {
val res = tryWithSuspend {
client.get(
"$apiUrl/users/@me",
authHeader ?: return@tryWithSuspend null
).parsed<MalUser>()
} ?: return false
MAL.userid = res.id
MAL.username = res.name
MAL.avatar = res.picture
return true
}
suspend fun editList(
idMAL: Int?,
isAnime: Boolean,
progress: Int?,
score: Int?,
status: String,
rewatch: Int? = null,
start: FuzzyDate? = null,
end: FuzzyDate? = null
) {
if(idMAL==null) return
val data = mutableMapOf("status" to convertStatus(isAnime, status))
if (progress != null)
data[if (isAnime) "num_watched_episodes" else "num_chapters_read"] = progress.toString()
data[if (isAnime) "is_rewatching" else "is_rereading"] = (status == "REPEATING").toString()
if (score != null)
data["score"] = score.div(10).toString()
if(rewatch!=null)
data[if(isAnime) "num_times_rewatched" else "num_times_reread"] = rewatch.toString()
if(start!=null)
data["start_date"] = start.toMALString()
if(end!=null)
data["finish_date"] = end.toMALString()
tryWithSuspend {
client.put(
"$apiUrl/${if (isAnime) "anime" else "manga"}/$idMAL/my_list_status",
authHeader ?: return@tryWithSuspend null,
data = data,
)
}
}
suspend fun deleteList(isAnime: Boolean, idMAL: Int?){
if(idMAL==null) return
tryWithSuspend {
client.delete(
"$apiUrl/${if (isAnime) "anime" else "manga"}/$idMAL/my_list_status",
authHeader ?: return@tryWithSuspend null
)
}
}
private fun convertStatus(isAnime: Boolean, status: String): String {
return when (status) {
"PLANNING" -> if (isAnime) "plan_to_watch" else "plan_to_read"
"COMPLETED" -> "completed"
"PAUSED" -> "on_hold"
"DROPPED" -> "dropped"
"CURRENT" -> if (isAnime) "watching" else "reading"
else -> if (isAnime) "watching" else "reading"
}
}
}