@@ -1,14 +1,14 @@
1package org.telegram.messenger.forkgram
1package org.telegram.messenger.forkgram
3import android.content.SharedPreferences
3import android.content.SharedPreferences
4−import android.os.Environment
4+import android.util.Base64
5import org.json.JSONObject
5import org.json.JSONObject
6−import org.telegram.messenger.ApplicationLoader
7import org.telegram.messenger.FileLog
6import org.telegram.messenger.FileLog
8import org.telegram.messenger.MessagesController
7import org.telegram.messenger.MessagesController
9import org.telegram.messenger.UserConfig
8import org.telegram.messenger.UserConfig
10−import java.io.File
9+import java.io.ByteArrayOutputStream
10+import java.util.zip.Deflater
11−import java.net.URL
11+import java.util.zip.Inflater
13object VoiceTimecodesConfig {
13object VoiceTimecodesConfig {
@@ -21,6 +21,20 @@ object VoiceTimecodesConfig {
21 const val PREF_MIN_DURATION = "voice_timecodes_min_duration"
21 const val PREF_MIN_DURATION = "voice_timecodes_min_duration"
22 const val PREF_PROMPT = "voice_timecodes_prompt"
22 const val PREF_PROMPT = "voice_timecodes_prompt"
24+ private const val CODE_PREFIX = "FVT1:"
25+ private const val MAX_DECODED_SIZE = 256 * 1024
26+ private val BASE64_FLAGS = Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING
27+ private val PREF_KEYS = setOf(
24 const val DEFAULT_LLM_MODEL = "gpt-4o-mini"
38 const val DEFAULT_LLM_MODEL = "gpt-4o-mini"
25 const val DEFAULT_MIN_DURATION = 10
39 const val DEFAULT_MIN_DURATION = 10
@@ -40,7 +54,6 @@ object VoiceTimecodesConfig {
41 @JvmStatic
55 @JvmStatic
42 fun isEnabled(): Boolean {
56 fun isEnabled(): Boolean {
44 return prefs().getBoolean(PREF_ENABLED, false)
57 return prefs().getBoolean(PREF_ENABLED, false)
@@ -51,31 +64,26 @@ object VoiceTimecodesConfig {
52 @JvmStatic
65 @JvmStatic
53 fun sttUrl(): String {
66 fun sttUrl(): String {
55 return prefs().getString(PREF_STT_URL, "")?.trim() ?: ""
67 return prefs().getString(PREF_STT_URL, "")?.trim() ?: ""
58 @JvmStatic
70 @JvmStatic
59 fun sttToken(): String {
71 fun sttToken(): String {
61 return prefs().getString(PREF_STT_TOKEN, "")?.trim() ?: ""
72 return prefs().getString(PREF_STT_TOKEN, "")?.trim() ?: ""
64 @JvmStatic
75 @JvmStatic
65 fun llmUrl(): String {
76 fun llmUrl(): String {
67 return prefs().getString(PREF_LLM_URL, "")?.trim() ?: ""
77 return prefs().getString(PREF_LLM_URL, "")?.trim() ?: ""
70 @JvmStatic
80 @JvmStatic
71 fun llmToken(): String {
81 fun llmToken(): String {
73 return prefs().getString(PREF_LLM_TOKEN, "")?.trim() ?: ""
82 return prefs().getString(PREF_LLM_TOKEN, "")?.trim() ?: ""
76 @JvmStatic
85 @JvmStatic
77 fun llmModel(): String {
86 fun llmModel(): String {
79 val m = prefs().getString(PREF_LLM_MODEL, DEFAULT_LLM_MODEL)?.trim() ?: ""
87 val m = prefs().getString(PREF_LLM_MODEL, DEFAULT_LLM_MODEL)?.trim() ?: ""
80 return if (m.isEmpty()) DEFAULT_LLM_MODEL else m
88 return if (m.isEmpty()) DEFAULT_LLM_MODEL else m
@@ -125,62 +133,57 @@ object VoiceTimecodesConfig {
129− private var autoImportChecked = false
131 @JvmStatic
136 @JvmStatic
132− fun ensureAutoImport() {
137+ fun exportToString(): String {
133− if (autoImportChecked) return
138+ return exportToString(
134− autoImportChecked = true
139+ isEnabled(), sttUrl(), sttToken(), llmUrl(), llmToken(), llmModel(), minDuration(), promptTemplate()
136− val currentStt = prefs().getString(PREF_STT_URL, null)
143+ @JvmStatic
137− val currentLlm = prefs().getString(PREF_LLM_URL, null)
144+ fun exportToString(
138− if (!currentStt.isNullOrEmpty() && !currentLlm.isNullOrEmpty()) {
145+ enabled: Boolean,
154+ val root = JSONObject()
156+ root.put("enabled", enabled)
157+ root.put("stt_url", sttUrl.trim())
158+ root.put("stt_token", sttToken.trim())
159+ root.put("llm_url", llmUrl.trim())
160+ root.put("llm_token", llmToken.trim())
161+ root.put("llm_model", llmModel.trim())
162+ root.put("min_duration", if (minDuration <= 0) DEFAULT_MIN_DURATION else minDuration)
163+ root.put("prompt", if (prompt.isBlank()) DEFAULT_PROMPT else prompt)
164+ val encoded = Base64.encodeToString(compress(root.toString().toByteArray(Charsets.UTF_8)), BASE64_FLAGS)
139− return
165+ return CODE_PREFIX + encoded
142− val candidateDirs = mutableListOf<File>()
168+ @JvmStatic
143− try {
169+ fun importFromString(text: String): Boolean {
144− val downloadDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
170+ val trimmed = text.trim()
145− if (downloadDir != null) {
171+ if (trimmed.isEmpty()) {
146− candidateDirs.add(downloadDir)
172+ return false
148− } catch (e: Exception) {
151− val context = ApplicationLoader.applicationContext
174+ val prefixIndex = trimmed.indexOf(CODE_PREFIX, ignoreCase = true)
152− if (context != null) {
175+ if (prefixIndex >= 0) {
153− try {
176+ return try {
154− val extFiles = context.getExternalFilesDir(null)
177+ val payload = trimmed.substring(prefixIndex + CODE_PREFIX.length)
155− if (extFiles != null) {
178+ .filter { it.isLetterOrDigit() || it == '-' || it == '_' }
156− candidateDirs.add(extFiles)
179+ val json = String(inflate(Base64.decode(payload, BASE64_FLAGS)), Charsets.UTF_8)
157− }
180+ importFromJson(json)
158 } catch (e: Exception) {
181 } catch (e: Exception) {
159− FileLog.e(e)
182+ FileLog.e("VoiceTimecodesConfig code import error: $e")
163− for (dir in candidateDirs) {
164− val f1 = File(dir, "voice_timecodes.json")
165− if (f1.exists() && f1.canRead() && importFromFile(f1)) {
168− val f2 = File(dir, "voice_timecodes_backup.json")
169− if (f2.exists() && f2.canRead() && importFromFile(f2)) {
176− fun importFromFile(file: File): Boolean {
178− val text = file.readText(Charsets.UTF_8)
179− importFromJson(text)
180− } catch (e: Exception) {
181− FileLog.e("VoiceTimecodesConfig import error from ${file.absolutePath}: $e")
186+ return importFromJson(trimmed)
186 @JvmStatic
189 @JvmStatic
@@ -188,68 +191,112 @@ object VoiceTimecodesConfig {
188 return try {
191 return try {
189 val root = JSONObject(jsonString)
192 val root = JSONObject(jsonString)
190 val editor = prefs().edit()
193 val editor = prefs().edit()
192− if (root.has("prefs")) {
193− val prefsObj = root.optJSONObject("prefs")
196+ val mainConfig = root.optJSONObject("prefs")?.optJSONObject("mainconfig")
194− val mainConfig = prefsObj?.optJSONObject("mainconfig")
195− if (mainConfig != null) {
197+ if (mainConfig != null) {
196− val it = mainConfig.keys()
198+ val keys = mainConfig.keys()
197− while (it.hasNext()) {
199+ while (keys.hasNext()) {
198− val key = it.next()
200+ val key = keys.next()
201+ if (key !in PREF_KEYS) {
199− val item = mainConfig.optJSONObject(key) ?: continue
204+ val item = mainConfig.optJSONObject(key) ?: continue
200− val t = item.optString("t")
201− when (t) {
205+ when (item.optString("t")) {
202− "b" -> editor.putBoolean(key, item.optBoolean("v"))
207+ editor.putBoolean(key, item.optBoolean("v"))
203− "s" -> editor.putString(key, item.optString("v"))
211+ editor.putString(key, item.optString("v"))
204− "i" -> editor.putInt(key, item.optInt("v"))
215+ editor.putInt(key, item.optInt("v"))
207− editor.apply()
219+ }
208− return true
220+ } else {
221+ if (root.has("enabled")) {
222+ editor.putBoolean(PREF_ENABLED, root.optBoolean("enabled"))
225+ if (root.has("stt_url")) {
226+ editor.putString(PREF_STT_URL, root.optString("stt_url"))
229+ if (root.has("stt_token")) {
230+ editor.putString(PREF_STT_TOKEN, root.optString("stt_token"))
233+ if (root.has("llm_url")) {
234+ editor.putString(PREF_LLM_URL, root.optString("llm_url"))
237+ if (root.has("llm_token")) {
238+ editor.putString(PREF_LLM_TOKEN, root.optString("llm_token"))
241+ if (root.has("llm_model")) {
242+ editor.putString(PREF_LLM_MODEL, root.optString("llm_model"))
245+ if (root.has("min_duration")) {
246+ editor.putInt(PREF_MIN_DURATION, root.optInt("min_duration", DEFAULT_MIN_DURATION))
249+ if (root.has("prompt")) {
250+ editor.putString(PREF_PROMPT, root.optString("prompt"))
213− if (root.has("enabled")) {
214− editor.putBoolean(PREF_ENABLED, root.getBoolean("enabled"))
217− if (root.has("stt_url")) {
218− editor.putString(PREF_STT_URL, root.getString("stt_url"))
221− if (root.has("stt_token")) {
222− editor.putString(PREF_STT_TOKEN, root.getString("stt_token"))
225− if (root.has("llm_url")) {
226− editor.putString(PREF_LLM_URL, root.getString("llm_url"))
229− if (root.has("llm_token")) {
230− editor.putString(PREF_LLM_TOKEN, root.getString("llm_token"))
233− if (root.has("llm_model")) {
234− editor.putString(PREF_LLM_MODEL, root.getString("llm_model"))
237− if (root.has("min_duration")) {
238− editor.putInt(PREF_MIN_DURATION, root.getInt("min_duration"))
241− if (root.has("prompt")) {
242− editor.putString(PREF_PROMPT, root.getString("prompt"))
245 if (updated) {
255 if (updated) {
246 editor.apply()
256 editor.apply()
250 } catch (e: Exception) {
259 } catch (e: Exception) {
251 FileLog.e(e)
260 FileLog.e(e)
265+ private fun compress(data: ByteArray): ByteArray {
266+ val deflater = Deflater(Deflater.BEST_COMPRESSION)
267+ val out = ByteArrayOutputStream()
268+ val buffer = ByteArray(4096)
270+ deflater.setInput(data)
272+ while (!deflater.finished()) {
273+ out.write(buffer, 0, deflater.deflate(buffer))
278+ return out.toByteArray()
281+ private fun inflate(data: ByteArray): ByteArray {
282+ val inflater = Inflater()
283+ val out = ByteArrayOutputStream()
284+ val buffer = ByteArray(4096)
286+ inflater.setInput(data)
287+ while (!inflater.finished()) {
288+ val count = inflater.inflate(buffer)
290+ throw IllegalArgumentException("Truncated settings code")
292+ out.write(buffer, 0, count)
293+ if (out.size() > MAX_DECODED_SIZE) {
294+ throw IllegalArgumentException("Settings code is too large")
300+ return out.toByteArray()