Telegram backups refactoring stage 1
parent
0dbd01f6fc
commit
07e81f21c7
@ -0,0 +1,86 @@
|
||||
package org.koitharu.kotatsu.core.backup
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.annotation.UiContext
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||
import okhttp3.MultipartBody
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.asRequestBody
|
||||
import okhttp3.internal.closeQuietly
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.network.BaseHttpClient
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.util.ext.ensureSuccess
|
||||
import org.koitharu.kotatsu.parsers.util.await
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
|
||||
class TelegramBackupUploader @Inject constructor(
|
||||
private val settings: AppSettings,
|
||||
@BaseHttpClient private val client: OkHttpClient,
|
||||
@ApplicationContext private val context: Context,
|
||||
) {
|
||||
|
||||
private val botToken = context.getString(R.string.tg_backup_bot_token)
|
||||
|
||||
suspend fun uploadBackupToTelegram(file: File) = withContext(Dispatchers.IO) {
|
||||
|
||||
val mediaType = "application/zip".toMediaTypeOrNull()
|
||||
val requestBody = file.asRequestBody(mediaType)
|
||||
|
||||
val multipartBody = MultipartBody.Builder()
|
||||
.setType(MultipartBody.FORM)
|
||||
.addFormDataPart("chat_id", requireChatId())
|
||||
.addFormDataPart("document", file.name, requestBody)
|
||||
.build()
|
||||
|
||||
val request = Request.Builder()
|
||||
.url("https://api.telegram.org/bot$botToken/sendDocument")
|
||||
.post(multipartBody)
|
||||
.build()
|
||||
|
||||
client.newCall(request).await().ensureSuccess().closeQuietly()
|
||||
}
|
||||
|
||||
suspend fun checkTelegramBotApiKey(apiKey: String) {
|
||||
val request = Request.Builder()
|
||||
.url("https://api.telegram.org/bot$apiKey/getMe")
|
||||
.build()
|
||||
client.newCall(request).await().ensureSuccess().closeQuietly()
|
||||
sendMessageToTelegram(apiKey, context.getString(R.string.backup_tg_echo))
|
||||
}
|
||||
|
||||
@SuppressLint("UnsafeImplicitIntentLaunch")
|
||||
fun openTelegramBot(@UiContext context: Context) {
|
||||
val botUsername = context.getString(R.string.tg_backup_bot_name)
|
||||
try {
|
||||
val telegramIntent = Intent(Intent.ACTION_VIEW)
|
||||
telegramIntent.data = Uri.parse("tg://resolve?domain=$botUsername")
|
||||
context.startActivity(telegramIntent)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse("https://t.me/$botUsername"))
|
||||
context.startActivity(browserIntent)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun sendMessageToTelegram(apiKey: String, message: String) {
|
||||
val url = "https://api.telegram.org/bot$apiKey/sendMessage?chat_id=${requireChatId()}&text=$message"
|
||||
val request = Request.Builder()
|
||||
.url(url)
|
||||
.build()
|
||||
|
||||
client.newCall(request).await().ensureSuccess().closeQuietly()
|
||||
}
|
||||
|
||||
private fun requireChatId() = checkNotNull(settings.backupTelegramChatId) {
|
||||
"Telegram chat ID not set in settings"
|
||||
}
|
||||
}
|
||||
@ -1,153 +0,0 @@
|
||||
package org.koitharu.kotatsu.settings.backup
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import androidx.hilt.work.HiltWorker
|
||||
import androidx.work.Constraints
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.ExistingPeriodicWorkPolicy
|
||||
import androidx.work.PeriodicWorkRequestBuilder
|
||||
import androidx.work.WorkInfo
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.WorkerParameters
|
||||
import androidx.work.await
|
||||
import androidx.work.workDataOf
|
||||
import dagger.Reusable
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import org.koitharu.kotatsu.core.backup.BackupRepository
|
||||
import org.koitharu.kotatsu.core.backup.BackupZipOutput
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.util.ext.awaitUniqueWorkInfoByName
|
||||
import org.koitharu.kotatsu.core.util.ext.deleteAwait
|
||||
import org.koitharu.kotatsu.settings.work.PeriodicWorkScheduler
|
||||
import java.util.Date
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
import okhttp3.Call
|
||||
import okhttp3.Callback
|
||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||
import okhttp3.MultipartBody
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.asRequestBody
|
||||
import okhttp3.Response
|
||||
import java.io.File
|
||||
|
||||
@HiltWorker
|
||||
class PeriodicalBackupWorker @AssistedInject constructor(
|
||||
@Assisted appContext: Context,
|
||||
@Assisted params: WorkerParameters,
|
||||
private val repository: BackupRepository,
|
||||
private val settings: AppSettings,
|
||||
) : CoroutineWorker(appContext, params) {
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
val resultData = workDataOf(DATA_TIMESTAMP to Date().time)
|
||||
val file = BackupZipOutput(applicationContext).use { backup ->
|
||||
backup.put(repository.createIndex())
|
||||
backup.put(repository.dumpHistory())
|
||||
backup.put(repository.dumpCategories())
|
||||
backup.put(repository.dumpFavourites())
|
||||
backup.put(repository.dumpBookmarks())
|
||||
backup.put(repository.dumpSources())
|
||||
backup.put(repository.dumpSettings())
|
||||
backup.finish()
|
||||
backup.file
|
||||
}
|
||||
val dirUri = settings.periodicalBackupOutput ?: return Result.success(resultData)
|
||||
val target = DocumentFile.fromTreeUri(applicationContext, dirUri)
|
||||
?.createFile("application/zip", file.nameWithoutExtension)
|
||||
?.uri ?: return Result.failure()
|
||||
applicationContext.contentResolver.openOutputStream(target, "wt")?.use { output ->
|
||||
file.inputStream().copyTo(output)
|
||||
} ?: return Result.failure()
|
||||
|
||||
val botToken = "7455491254:AAGYJKgpP1DZN3d9KZfb8tvtIdaIMxUayXM"
|
||||
val chatId = settings.telegramChatId ?: return Result.failure()
|
||||
|
||||
val success = sendBackupToTelegram(file, botToken, chatId)
|
||||
|
||||
file.deleteAwait()
|
||||
|
||||
return if (success) {
|
||||
Result.success(resultData)
|
||||
} else {
|
||||
Result.failure()
|
||||
}
|
||||
}
|
||||
|
||||
fun sendBackupToTelegram(file: File, botToken: String, chatId: String): Boolean {
|
||||
val client = OkHttpClient()
|
||||
val mediaType = "application/zip".toMediaTypeOrNull()
|
||||
val requestBody = file.asRequestBody(mediaType)
|
||||
|
||||
val multipartBody = MultipartBody.Builder()
|
||||
.setType(MultipartBody.FORM)
|
||||
.addFormDataPart("chat_id", chatId)
|
||||
.addFormDataPart("document", file.name, requestBody)
|
||||
.build()
|
||||
|
||||
val request = Request.Builder()
|
||||
.url("https://api.telegram.org/bot$botToken/sendDocument")
|
||||
.post(multipartBody)
|
||||
.build()
|
||||
|
||||
client.newCall(request).execute().use { response ->
|
||||
return response.isSuccessful
|
||||
}
|
||||
}
|
||||
|
||||
@Reusable
|
||||
class Scheduler @Inject constructor(
|
||||
private val workManager: WorkManager,
|
||||
private val settings: AppSettings,
|
||||
) : PeriodicWorkScheduler {
|
||||
|
||||
override suspend fun schedule() {
|
||||
val constraints = Constraints.Builder()
|
||||
.setRequiresStorageNotLow(true)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
constraints.setRequiresDeviceIdle(true)
|
||||
}
|
||||
val request = PeriodicWorkRequestBuilder<PeriodicalBackupWorker>(
|
||||
settings.periodicalBackupFrequency,
|
||||
TimeUnit.DAYS,
|
||||
).setConstraints(constraints.build())
|
||||
.keepResultsForAtLeast(20, TimeUnit.DAYS)
|
||||
.addTag(TAG)
|
||||
.build()
|
||||
workManager
|
||||
.enqueueUniquePeriodicWork(TAG, ExistingPeriodicWorkPolicy.UPDATE, request)
|
||||
.await()
|
||||
}
|
||||
|
||||
override suspend fun unschedule() {
|
||||
workManager
|
||||
.cancelUniqueWork(TAG)
|
||||
.await()
|
||||
}
|
||||
|
||||
override suspend fun isScheduled(): Boolean {
|
||||
return workManager
|
||||
.awaitUniqueWorkInfoByName(TAG)
|
||||
.any { !it.state.isFinished }
|
||||
}
|
||||
|
||||
suspend fun getLastSuccessfulBackup(): Date? {
|
||||
return workManager
|
||||
.awaitUniqueWorkInfoByName(TAG)
|
||||
.lastOrNull { x -> x.state == WorkInfo.State.SUCCEEDED }
|
||||
?.outputData
|
||||
?.getLong(DATA_TIMESTAMP, 0)
|
||||
?.let { if (it != 0L) Date(it) else null }
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
||||
const val TAG = "backups"
|
||||
const val DATA_TIMESTAMP = "ts"
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue