That fuck shit the fascists are using
1package org.tm.archive.database
2
3import android.content.ContentValues
4import android.content.Context
5import android.database.Cursor
6import android.net.Uri
7import androidx.core.content.contentValuesOf
8import androidx.core.net.toUri
9import org.json.JSONException
10import org.json.JSONObject
11import org.signal.core.util.deleteAll
12import org.signal.core.util.logging.Log
13import org.signal.core.util.readToList
14import org.signal.core.util.requireInt
15import org.signal.core.util.requireLong
16import org.signal.core.util.requireNonNullString
17import org.signal.core.util.requireString
18import org.signal.core.util.select
19import org.signal.core.util.update
20import org.tm.archive.BuildConfig
21import org.tm.archive.database.model.RemoteMegaphoneRecord
22import java.util.concurrent.TimeUnit
23
24/**
25 * Stores remotely configured megaphones.
26 */
27class RemoteMegaphoneTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTable(context, databaseHelper) {
28
29 companion object {
30 private val TAG = Log.tag(RemoteMegaphoneTable::class.java)
31
32 private const val TABLE_NAME = "remote_megaphone"
33 private const val ID = "_id"
34 private const val UUID = "uuid"
35 private const val COUNTRIES = "countries"
36 private const val PRIORITY = "priority"
37 private const val MINIMUM_VERSION = "minimum_version"
38 private const val DONT_SHOW_BEFORE = "dont_show_before"
39 private const val DONT_SHOW_AFTER = "dont_show_after"
40 private const val SHOW_FOR_DAYS = "show_for_days"
41 private const val CONDITIONAL_ID = "conditional_id"
42 private const val PRIMARY_ACTION_ID = "primary_action_id"
43 private const val SECONDARY_ACTION_ID = "secondary_action_id"
44 private const val IMAGE_URL = "image_url"
45 private const val IMAGE_BLOB_URI = "image_uri"
46 private const val TITLE = "title"
47 private const val BODY = "body"
48 private const val PRIMARY_ACTION_TEXT = "primary_action_text"
49 private const val SECONDARY_ACTION_TEXT = "secondary_action_text"
50 private const val SHOWN_AT = "shown_at"
51 private const val FINISHED_AT = "finished_at"
52 private const val PRIMARY_ACTION_DATA = "primary_action_data"
53 private const val SECONDARY_ACTION_DATA = "secondary_action_data"
54 private const val SNOOZED_AT = "snoozed_at"
55 private const val SEEN_COUNT = "seen_count"
56
57 val CREATE_TABLE = """
58 CREATE TABLE $TABLE_NAME (
59 $ID INTEGER PRIMARY KEY,
60 $UUID TEXT UNIQUE NOT NULL,
61 $PRIORITY INTEGER NOT NULL,
62 $COUNTRIES TEXT,
63 $MINIMUM_VERSION INTEGER NOT NULL,
64 $DONT_SHOW_BEFORE INTEGER NOT NULL,
65 $DONT_SHOW_AFTER INTEGER NOT NULL,
66 $SHOW_FOR_DAYS INTEGER NOT NULL,
67 $CONDITIONAL_ID TEXT,
68 $PRIMARY_ACTION_ID TEXT,
69 $SECONDARY_ACTION_ID TEXT,
70 $IMAGE_URL TEXT,
71 $IMAGE_BLOB_URI TEXT DEFAULT NULL,
72 $TITLE TEXT NOT NULL,
73 $BODY TEXT NOT NULL,
74 $PRIMARY_ACTION_TEXT TEXT,
75 $SECONDARY_ACTION_TEXT TEXT,
76 $SHOWN_AT INTEGER DEFAULT 0,
77 $FINISHED_AT INTEGER DEFAULT 0,
78 $PRIMARY_ACTION_DATA TEXT DEFAULT NULL,
79 $SECONDARY_ACTION_DATA TEXT DEFAULT NULL,
80 $SNOOZED_AT INTEGER DEFAULT 0,
81 $SEEN_COUNT INTEGER DEFAULT 0
82 )
83 """
84
85 const val VERSION_FINISHED = Int.MAX_VALUE
86 }
87
88 fun insert(record: RemoteMegaphoneRecord) {
89 writableDatabase.insert(TABLE_NAME, SQLiteDatabase.CONFLICT_REPLACE, record.toContentValues())
90 }
91
92 fun update(uuid: String, priority: Long, countries: String?, title: String, body: String, primaryActionText: String?, secondaryActionText: String?) {
93 writableDatabase
94 .update(TABLE_NAME)
95 .values(
96 PRIORITY to priority,
97 COUNTRIES to countries,
98 TITLE to title,
99 BODY to body,
100 PRIMARY_ACTION_TEXT to primaryActionText,
101 SECONDARY_ACTION_TEXT to secondaryActionText
102 )
103 .where("$UUID = ?", uuid)
104 .run()
105 }
106
107 fun getAll(): List<RemoteMegaphoneRecord> {
108 return readableDatabase
109 .select()
110 .from(TABLE_NAME)
111 .run()
112 .readToList { it.toRemoteMegaphoneRecord() }
113 }
114
115 fun getPotentialMegaphonesAndClearOld(now: Long): List<RemoteMegaphoneRecord> {
116 val records: List<RemoteMegaphoneRecord> = readableDatabase
117 .select()
118 .from(TABLE_NAME)
119 .where("$FINISHED_AT = ? AND $MINIMUM_VERSION <= ? AND ($DONT_SHOW_AFTER > ? AND $DONT_SHOW_BEFORE < ?)", 0, BuildConfig.CANONICAL_VERSION_CODE, now, now)
120 .orderBy("$PRIORITY DESC")
121 .run()
122 .readToList { it.toRemoteMegaphoneRecord() }
123
124 val oldRecords: Set<RemoteMegaphoneRecord> = records
125 .filter { it.shownAt > 0 && it.showForNumberOfDays > 0 }
126 .filter { it.shownAt + TimeUnit.DAYS.toMillis(it.showForNumberOfDays) < now }
127 .toSet()
128
129 for (oldRecord in oldRecords) {
130 clear(oldRecord.uuid)
131 }
132
133 return records - oldRecords
134 }
135
136 fun setImageUri(uuid: String, uri: Uri?) {
137 writableDatabase
138 .update(TABLE_NAME)
139 .values(IMAGE_BLOB_URI to uri?.toString())
140 .where("$UUID = ?", uuid)
141 .run()
142 }
143
144 fun markShown(uuid: String) {
145 writableDatabase
146 .update(TABLE_NAME)
147 .values(SHOWN_AT to System.currentTimeMillis())
148 .where("$UUID = ?", uuid)
149 .run()
150 }
151
152 fun markFinished(uuid: String) {
153 writableDatabase
154 .update(TABLE_NAME)
155 .values(
156 IMAGE_URL to null,
157 IMAGE_BLOB_URI to null,
158 FINISHED_AT to System.currentTimeMillis()
159 )
160 .where("$UUID = ?", uuid)
161 .run()
162 }
163
164 fun snooze(remote: RemoteMegaphoneRecord) {
165 writableDatabase
166 .update(TABLE_NAME)
167 .values(
168 SEEN_COUNT to remote.seenCount + 1,
169 SNOOZED_AT to System.currentTimeMillis()
170 )
171 .where("$UUID = ?", remote.uuid)
172 .run()
173 }
174
175 fun clearImageUrl(uuid: String) {
176 writableDatabase
177 .update(TABLE_NAME)
178 .values(IMAGE_URL to null)
179 .where("$UUID = ?", uuid)
180 .run()
181 }
182
183 fun clear(uuid: String) {
184 writableDatabase
185 .update(TABLE_NAME)
186 .values(
187 MINIMUM_VERSION to VERSION_FINISHED,
188 IMAGE_URL to null,
189 IMAGE_BLOB_URI to null
190 )
191 .where("$UUID = ?", uuid)
192 .run()
193 }
194
195 /** Only call from internal settings */
196 fun debugRemoveAll() {
197 writableDatabase.deleteAll(TABLE_NAME)
198 }
199
200 private fun RemoteMegaphoneRecord.toContentValues(): ContentValues {
201 return contentValuesOf(
202 UUID to uuid,
203 PRIORITY to priority,
204 COUNTRIES to countries,
205 MINIMUM_VERSION to minimumVersion,
206 DONT_SHOW_BEFORE to doNotShowBefore,
207 DONT_SHOW_AFTER to doNotShowAfter,
208 SHOW_FOR_DAYS to showForNumberOfDays,
209 CONDITIONAL_ID to conditionalId,
210 PRIMARY_ACTION_ID to primaryActionId?.id,
211 SECONDARY_ACTION_ID to secondaryActionId?.id,
212 IMAGE_URL to imageUrl,
213 TITLE to title,
214 BODY to body,
215 PRIMARY_ACTION_TEXT to primaryActionText,
216 SECONDARY_ACTION_TEXT to secondaryActionText,
217 FINISHED_AT to finishedAt,
218 PRIMARY_ACTION_DATA to primaryActionData?.toString(),
219 SECONDARY_ACTION_DATA to secondaryActionData?.toString(),
220 SNOOZED_AT to snoozedAt,
221 SEEN_COUNT to seenCount
222 )
223 }
224
225 private fun Cursor.toRemoteMegaphoneRecord(): RemoteMegaphoneRecord {
226 return RemoteMegaphoneRecord(
227 id = requireLong(ID),
228 uuid = requireNonNullString(UUID),
229 priority = requireLong(PRIORITY),
230 countries = requireString(COUNTRIES),
231 minimumVersion = requireInt(MINIMUM_VERSION),
232 doNotShowBefore = requireLong(DONT_SHOW_BEFORE),
233 doNotShowAfter = requireLong(DONT_SHOW_AFTER),
234 showForNumberOfDays = requireLong(SHOW_FOR_DAYS),
235 conditionalId = requireString(CONDITIONAL_ID),
236 primaryActionId = RemoteMegaphoneRecord.ActionId.from(requireString(PRIMARY_ACTION_ID)),
237 secondaryActionId = RemoteMegaphoneRecord.ActionId.from(requireString(SECONDARY_ACTION_ID)),
238 imageUrl = requireString(IMAGE_URL),
239 imageUri = requireString(IMAGE_BLOB_URI)?.toUri(),
240 title = requireNonNullString(TITLE),
241 body = requireNonNullString(BODY),
242 primaryActionText = requireString(PRIMARY_ACTION_TEXT),
243 secondaryActionText = requireString(SECONDARY_ACTION_TEXT),
244 shownAt = requireLong(SHOWN_AT),
245 finishedAt = requireLong(FINISHED_AT),
246 primaryActionData = requireString(PRIMARY_ACTION_DATA).parseJsonObject(),
247 secondaryActionData = requireString(SECONDARY_ACTION_DATA).parseJsonObject(),
248 snoozedAt = requireLong(SNOOZED_AT),
249 seenCount = requireInt(SEEN_COUNT)
250 )
251 }
252
253 private fun String?.parseJsonObject(): JSONObject? {
254 if (this == null) {
255 return null
256 }
257
258 return try {
259 JSONObject(this)
260 } catch (e: JSONException) {
261 Log.w(TAG, "Unable to parse data", e)
262 null
263 }
264 }
265}