Skip to content

Commit

Permalink
Telegram backups refactoring stage 1
Browse files Browse the repository at this point in the history
  • Loading branch information
Koitharu committed Dec 14, 2024
1 parent 0dbd01f commit 07e81f2
Show file tree
Hide file tree
Showing 8 changed files with 112 additions and 312 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,6 @@ import androidx.documentfile.provider.DocumentFile
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible
import kotlinx.coroutines.withContext
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.asRequestBody
import okio.buffer
import okio.sink
import okio.source
Expand All @@ -21,7 +15,6 @@ import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import java.io.File
import java.io.IOException
import javax.inject.Inject

class ExternalBackupStorage @Inject constructor(
Expand Down Expand Up @@ -96,36 +89,3 @@ class ExternalBackupStorage @Inject constructor(
return checkNotNull(root) { "Cannot obtain DocumentFile from $uri" }
}
}
class TelegramBackupUploader @Inject constructor(private val settings: AppSettings) {

private val client = OkHttpClient()

suspend fun uploadBackupToTelegram(file: File) = withContext(Dispatchers.IO) {
val botToken = "7455491254:AAGYJKgpP1DZN3d9KZfb8tvtIdaIMxUayXM"
val chatId = settings.telegramChatId

if (botToken.isNullOrEmpty() || chatId.isNullOrEmpty()) {
throw IllegalStateException("Telegram API key or chat ID not set in settings.")
}

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 ->
if (!response.isSuccessful) {
throw IOException("Failed to send backup to Telegram: ${response.message}")
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,6 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {

private val prefs = PreferenceManager.getDefaultSharedPreferences(context)
private val connectivityManager = context.connectivityManager
private val preferences: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)


var telegramChatId: String?
get() = preferences.getString("telegram_chat_id", null)
set(value) {
preferences.edit().putString("telegram_chat_id", value).apply()
}

var listMode: ListMode
get() = prefs.getEnumValue(KEY_LIST_MODE, ListMode.GRID)
Expand Down Expand Up @@ -497,6 +489,9 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
get() = prefs.getString(KEY_BACKUP_PERIODICAL_OUTPUT, null)?.toUriOrNull()
set(value) = prefs.edit { putString(KEY_BACKUP_PERIODICAL_OUTPUT, value?.toString()) }

val backupTelegramChatId: String?
get() = prefs.getString(KEY_BACKUP_TG_CHAT, null)

val isReadingTimeEstimationEnabled: Boolean
get() = prefs.getBoolean(KEY_READING_TIME, true)

Expand Down Expand Up @@ -724,6 +719,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
const val KEY_SEARCH_SUGGESTION_TYPES = "search_suggest_types"
const val KEY_SOURCES_VERSION = "sources_version"
const val KEY_QUICK_FILTER = "quick_filter"
const val KEY_BACKUP_TG_CHAT = "telegram_chat_id"

// keys for non-persistent preferences
const val KEY_APP_VERSION = "app_version"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import android.net.Uri
import android.os.Bundle
import android.text.format.DateUtils
import android.view.View
import android.widget.Toast
import androidx.activity.result.ActivityResultCallback
import androidx.activity.result.contract.ActivityResultContracts
import androidx.documentfile.provider.DocumentFile
Expand All @@ -18,19 +17,13 @@ import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.backup.BackupZipOutput.Companion.DIR_BACKUPS
import org.koitharu.kotatsu.core.backup.ExternalBackupStorage
import org.koitharu.kotatsu.core.backup.TelegramBackupUploader
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.BasePreferenceFragment
import org.koitharu.kotatsu.core.util.ext.resolveFile
import org.koitharu.kotatsu.core.util.ext.tryLaunch
import org.koitharu.kotatsu.core.util.ext.viewLifecycleScope
import okhttp3.Call
import okhttp3.Callback
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import java.io.File
import java.io.IOException
import java.text.SimpleDateFormat
import javax.inject.Inject

@AndroidEntryPoint
Expand All @@ -40,103 +33,21 @@ class PeriodicalBackupSettingsFragment : BasePreferenceFragment(R.string.periodi
@Inject
lateinit var backupStorage: ExternalBackupStorage

private val outputSelectCall = registerForActivityResult(
ActivityResultContracts.OpenDocumentTree(),
this,
)
@Inject
lateinit var telegramBackupUploader: TelegramBackupUploader

private val outputSelectCall = registerForActivityResult(ActivityResultContracts.OpenDocumentTree(), this)

override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.pref_backup_periodic)

val openTelegramBotPreference = findPreference<Preference>("open_telegram_chat")

openTelegramBotPreference?.setOnPreferenceClickListener {
openTelegramBot("kotatsu_backup_bot")
true
}
val checkApiButton = Preference(requireContext()).apply {
key = "check_api_working"
title = context.getString(R.string.api_telegram_check)
summary = context.getString(R.string.api_check_desc)
}

checkApiButton.setOnPreferenceClickListener {
val apiKey = "7455491254:AAGYJKgpP1DZN3d9KZfb8tvtIdaIMxUayXM"
if (apiKey.isNotEmpty()) {
checkTelegramBotApiKey(apiKey)
}
telegramBackupUploader.openTelegramBot(it.context, "kotatsu_backup_bot")
true
}

preferenceScreen.addPreference(checkApiButton)
}
private fun checkTelegramBotApiKey(apiKey: String) {
val url = "https://api.telegram.org/bot$apiKey/getMe"

val client = OkHttpClient()
val request = Request.Builder()
.url(url)
.build()

client.newCall(request).enqueue(object : Callback {
override fun onResponse(call: Call, response: Response) {
requireActivity().runOnUiThread {
if (response.isSuccessful) {
context?.let { sendMessageToTelegram(apiKey, it.getString(R.string.api_is_work)) }
}
}
}

override fun onFailure(call: Call, e: IOException) {
requireActivity().runOnUiThread {
Toast.makeText(requireContext(), R.string.api_net_error, Toast.LENGTH_SHORT).show()
}
}
})
}
private fun openTelegramBot(botUsername: String) {
try {
val telegramIntent = Intent(Intent.ACTION_VIEW)
telegramIntent.data = Uri.parse("https://t.me/$botUsername")
telegramIntent.setPackage("org.telegram.messenger")
startActivity(telegramIntent)
} catch (e: Exception) {
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse("https://t.me/$botUsername"))
startActivity(browserIntent)
}
}
private fun sendMessageToTelegram(apiKey: String, message: String) {
val chatId = settings.telegramChatId
if (chatId.isNullOrEmpty()) {
Toast.makeText(requireContext(), R.string.id_not_set, Toast.LENGTH_SHORT).show()
return
}

val url = "https://api.telegram.org/bot$apiKey/sendMessage?chat_id=$chatId&text=$message"
val client = OkHttpClient()
val request = Request.Builder()
.url(url)
.build()

client.newCall(request).enqueue(object : Callback {
override fun onResponse(call: Call, response: Response) {
requireActivity().runOnUiThread {
if (response.isSuccessful) {
Toast.makeText(requireContext(), R.string.api_check_success, Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(requireContext(), R.string.api_check_error, Toast.LENGTH_SHORT).show()
}
}
}

override fun onFailure(call: Call, e: IOException) {
requireActivity().runOnUiThread {
Toast.makeText(requireContext(), R.string.api_error, Toast.LENGTH_SHORT).show()
}
}
})
}


override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
Expand Down
Loading

0 comments on commit 07e81f2

Please sign in to comment.