@@ -1,14 +1,14 @@
11package org.telegram.messenger.forkgram
22
33import android.content.SharedPreferences
4−import android.os.Environment
4+import android.util.Base64
55import org.json.JSONObject
6−import org.telegram.messenger.ApplicationLoader
76import org.telegram.messenger.FileLog
87import org.telegram.messenger.MessagesController
98import org.telegram.messenger.UserConfig
10−import java.io.File
11−import java.net.URL
9+import java.io.ByteArrayOutputStream
10+import java.util.zip.Deflater
11+import java.util.zip.Inflater
1212
1313object VoiceTimecodesConfig {
1414
@@ -21,6 +21,20 @@ object VoiceTimecodesConfig {
2121 const val PREF_MIN_DURATION = "voice_timecodes_min_duration"
2222 const val PREF_PROMPT = "voice_timecodes_prompt"
2323
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(
28+ PREF_ENABLED,
29+ PREF_STT_URL,
30+ PREF_STT_TOKEN,
31+ PREF_LLM_URL,
32+ PREF_LLM_TOKEN,
33+ PREF_LLM_MODEL,
34+ PREF_MIN_DURATION,
35+ PREF_PROMPT
36+ )
37+
2438 const val DEFAULT_LLM_MODEL = "gpt-4o-mini"
2539 const val DEFAULT_MIN_DURATION = 10
2640
@@ -40,7 +54,6 @@ object VoiceTimecodesConfig {
4054
4155 @JvmStatic
4256 fun isEnabled(): Boolean {
43− ensureAutoImport()
4457 return prefs().getBoolean(PREF_ENABLED, false)
4558 }
4659
@@ -51,31 +64,26 @@ object VoiceTimecodesConfig {
5164
5265 @JvmStatic
5366 fun sttUrl(): String {
54− ensureAutoImport()
5567 return prefs().getString(PREF_STT_URL, "")?.trim() ?: ""
5668 }
5769
5870 @JvmStatic
5971 fun sttToken(): String {
60− ensureAutoImport()
6172 return prefs().getString(PREF_STT_TOKEN, "")?.trim() ?: ""
6273 }
6374
6475 @JvmStatic
6576 fun llmUrl(): String {
66− ensureAutoImport()
6777 return prefs().getString(PREF_LLM_URL, "")?.trim() ?: ""
6878 }
6979
7080 @JvmStatic
7181 fun llmToken(): String {
72− ensureAutoImport()
7382 return prefs().getString(PREF_LLM_TOKEN, "")?.trim() ?: ""
7483 }
7584
7685 @JvmStatic
7786 fun llmModel(): String {
78− ensureAutoImport()
7987 val m = prefs().getString(PREF_LLM_MODEL, DEFAULT_LLM_MODEL)?.trim() ?: ""
8088 return if (m.isEmpty()) DEFAULT_LLM_MODEL else m
8189 }
@@ -125,62 +133,57 @@ object VoiceTimecodesConfig {
125133 .apply()
126134 }
127135
128− @Volatile
129− private var autoImportChecked = false
130−
131136 @JvmStatic
132− fun ensureAutoImport() {
133− if (autoImportChecked) return
134− autoImportChecked = true
137+ fun exportToString(): String {
138+ return exportToString(
139+ isEnabled(), sttUrl(), sttToken(), llmUrl(), llmToken(), llmModel(), minDuration(), promptTemplate()
140+ )
141+ }
135142
136− val currentStt = prefs().getString(PREF_STT_URL, null)
137− val currentLlm = prefs().getString(PREF_LLM_URL, null)
138− if (!currentStt.isNullOrEmpty() && !currentLlm.isNullOrEmpty()) {
139− return
140− }
143+ @JvmStatic
144+ fun exportToString(
145+ enabled: Boolean,
146+ sttUrl: String,
147+ sttToken: String,
148+ llmUrl: String,
149+ llmToken: String,
150+ llmModel: String,
151+ minDuration: Int,
152+ prompt: String
153+ ): String {
154+ val root = JSONObject()
155+ root.put("v", 1)
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)
165+ return CODE_PREFIX + encoded
166+ }
141167
142− val candidateDirs = mutableListOf<File>()
143− try {
144− val downloadDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
145− if (downloadDir != null) {
146− candidateDirs.add(downloadDir)
147− }
148− } catch (e: Exception) {
149− FileLog.e(e)
168+ @JvmStatic
169+ fun importFromString(text: String): Boolean {
170+ val trimmed = text.trim()
171+ if (trimmed.isEmpty()) {
172+ return false
150173 }
151− val context = ApplicationLoader.applicationContext
152− if (context != null) {
153− try {
154− val extFiles = context.getExternalFilesDir(null)
155− if (extFiles != null) {
156− candidateDirs.add(extFiles)
157− }
174+ val prefixIndex = trimmed.indexOf(CODE_PREFIX, ignoreCase = true)
175+ if (prefixIndex >= 0) {
176+ return try {
177+ val payload = trimmed.substring(prefixIndex + CODE_PREFIX.length)
178+ .filter { it.isLetterOrDigit() || it == '-' || it == '_' }
179+ val json = String(inflate(Base64.decode(payload, BASE64_FLAGS)), Charsets.UTF_8)
180+ importFromJson(json)
158181 } catch (e: Exception) {
159− FileLog.e(e)
160− }
161− }
162−
163− for (dir in candidateDirs) {
164− val f1 = File(dir, "voice_timecodes.json")
165− if (f1.exists() && f1.canRead() && importFromFile(f1)) {
166− return
182+ FileLog.e("VoiceTimecodesConfig code import error: $e")
183+ false
167184 }
168− val f2 = File(dir, "voice_timecodes_backup.json")
169− if (f2.exists() && f2.canRead() && importFromFile(f2)) {
170− return
171− }
172− }
173− }
174−
175− @JvmStatic
176− fun importFromFile(file: File): Boolean {
177− return try {
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")
182− false
183185 }
186+ return importFromJson(trimmed)
184187 }
185188
186189 @JvmStatic
@@ -188,68 +191,112 @@ object VoiceTimecodesConfig {
188191 return try {
189192 val root = JSONObject(jsonString)
190193 val editor = prefs().edit()
194+ var updated = false
191195
192− if (root.has("prefs")) {
193− val prefsObj = root.optJSONObject("prefs")
194− val mainConfig = prefsObj?.optJSONObject("mainconfig")
195− if (mainConfig != null) {
196− val it = mainConfig.keys()
197− while (it.hasNext()) {
198− val key = it.next()
199− val item = mainConfig.optJSONObject(key) ?: continue
200− val t = item.optString("t")
201− when (t) {
202− "b" -> editor.putBoolean(key, item.optBoolean("v"))
203− "s" -> editor.putString(key, item.optString("v"))
204− "i" -> editor.putInt(key, item.optInt("v"))
196+ val mainConfig = root.optJSONObject("prefs")?.optJSONObject("mainconfig")
197+ if (mainConfig != null) {
198+ val keys = mainConfig.keys()
199+ while (keys.hasNext()) {
200+ val key = keys.next()
201+ if (key !in PREF_KEYS) {
202+ continue
203+ }
204+ val item = mainConfig.optJSONObject(key) ?: continue
205+ when (item.optString("t")) {
206+ "b" -> {
207+ editor.putBoolean(key, item.optBoolean("v"))
208+ updated = true
209+ }
210+ "s" -> {
211+ editor.putString(key, item.optString("v"))
212+ updated = true
213+ }
214+ "i" -> {
215+ editor.putInt(key, item.optInt("v"))
216+ updated = true
205217 }
206218 }
207− editor.apply()
208− return true
219+ }
220+ } else {
221+ if (root.has("enabled")) {
222+ editor.putBoolean(PREF_ENABLED, root.optBoolean("enabled"))
223+ updated = true
224+ }
225+ if (root.has("stt_url")) {
226+ editor.putString(PREF_STT_URL, root.optString("stt_url"))
227+ updated = true
228+ }
229+ if (root.has("stt_token")) {
230+ editor.putString(PREF_STT_TOKEN, root.optString("stt_token"))
231+ updated = true
232+ }
233+ if (root.has("llm_url")) {
234+ editor.putString(PREF_LLM_URL, root.optString("llm_url"))
235+ updated = true
236+ }
237+ if (root.has("llm_token")) {
238+ editor.putString(PREF_LLM_TOKEN, root.optString("llm_token"))
239+ updated = true
240+ }
241+ if (root.has("llm_model")) {
242+ editor.putString(PREF_LLM_MODEL, root.optString("llm_model"))
243+ updated = true
244+ }
245+ if (root.has("min_duration")) {
246+ editor.putInt(PREF_MIN_DURATION, root.optInt("min_duration", DEFAULT_MIN_DURATION))
247+ updated = true
248+ }
249+ if (root.has("prompt")) {
250+ editor.putString(PREF_PROMPT, root.optString("prompt"))
251+ updated = true
209252 }
210253 }
211254
212− var updated = false
213− if (root.has("enabled")) {
214− editor.putBoolean(PREF_ENABLED, root.getBoolean("enabled"))
215− updated = true
216− }
217− if (root.has("stt_url")) {
218− editor.putString(PREF_STT_URL, root.getString("stt_url"))
219− updated = true
220− }
221− if (root.has("stt_token")) {
222− editor.putString(PREF_STT_TOKEN, root.getString("stt_token"))
223− updated = true
224− }
225− if (root.has("llm_url")) {
226− editor.putString(PREF_LLM_URL, root.getString("llm_url"))
227− updated = true
228− }
229− if (root.has("llm_token")) {
230− editor.putString(PREF_LLM_TOKEN, root.getString("llm_token"))
231− updated = true
232− }
233− if (root.has("llm_model")) {
234− editor.putString(PREF_LLM_MODEL, root.getString("llm_model"))
235− updated = true
236− }
237− if (root.has("min_duration")) {
238− editor.putInt(PREF_MIN_DURATION, root.getInt("min_duration"))
239− updated = true
240− }
241− if (root.has("prompt")) {
242− editor.putString(PREF_PROMPT, root.getString("prompt"))
243− updated = true
244− }
245255 if (updated) {
246256 editor.apply()
247− return true
248257 }
249− false
258+ updated
250259 } catch (e: Exception) {
251260 FileLog.e(e)
252261 false
253262 }
254263 }
264+
265+ private fun compress(data: ByteArray): ByteArray {
266+ val deflater = Deflater(Deflater.BEST_COMPRESSION)
267+ val out = ByteArrayOutputStream()
268+ val buffer = ByteArray(4096)
269+ try {
270+ deflater.setInput(data)
271+ deflater.finish()
272+ while (!deflater.finished()) {
273+ out.write(buffer, 0, deflater.deflate(buffer))
274+ }
275+ } finally {
276+ deflater.end()
277+ }
278+ return out.toByteArray()
279+ }
280+
281+ private fun inflate(data: ByteArray): ByteArray {
282+ val inflater = Inflater()
283+ val out = ByteArrayOutputStream()
284+ val buffer = ByteArray(4096)
285+ try {
286+ inflater.setInput(data)
287+ while (!inflater.finished()) {
288+ val count = inflater.inflate(buffer)
289+ if (count == 0) {
290+ throw IllegalArgumentException("Truncated settings code")
291+ }
292+ out.write(buffer, 0, count)
293+ if (out.size() > MAX_DECODED_SIZE) {
294+ throw IllegalArgumentException("Settings code is too large")
295+ }
296+ }
297+ } finally {
298+ inflater.end()
299+ }
300+ return out.toByteArray()
301+ }
255302}