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.text.TextUtils;
7import android.util.Pair;
8
9import androidx.annotation.NonNull;
10import androidx.annotation.Nullable;
11
12import org.greenrobot.eventbus.EventBus;
13import org.signal.core.util.StreamUtil;
14import org.signal.core.util.logging.Log;
15import org.tm.archive.crypto.AttachmentSecret;
16import org.tm.archive.crypto.ModernDecryptingPartInputStream;
17import org.tm.archive.crypto.ModernEncryptingPartOutputStream;
18import org.tm.archive.database.model.IncomingSticker;
19import org.tm.archive.database.model.StickerPackRecord;
20import org.tm.archive.database.model.StickerRecord;
21import org.tm.archive.mms.DecryptableStreamUriLoader.DecryptableUri;
22import org.tm.archive.stickers.BlessedPacks;
23import org.tm.archive.stickers.StickerPackInstallEvent;
24import org.signal.core.util.CursorUtil;
25import org.signal.core.util.SqlUtil;
26
27import java.io.Closeable;
28import java.io.File;
29import java.io.IOException;
30import java.io.InputStream;
31import java.io.OutputStream;
32import java.util.HashSet;
33import java.util.List;
34import java.util.Set;
35
36public class StickerTable extends DatabaseTable {
37
38 private static final String TAG = Log.tag(StickerTable.class);
39
40 public static final String TABLE_NAME = "sticker";
41 public static final String _ID = "_id";
42 static final String PACK_ID = "pack_id";
43 private static final String PACK_KEY = "pack_key";
44 private static final String PACK_TITLE = "pack_title";
45 private static final String PACK_AUTHOR = "pack_author";
46 private static final String STICKER_ID = "sticker_id";
47 private static final String EMOJI = "emoji";
48 public static final String CONTENT_TYPE = "content_type";
49 private static final String COVER = "cover";
50 private static final String PACK_ORDER = "pack_order";
51 private static final String INSTALLED = "installed";
52 private static final String LAST_USED = "last_used";
53 public static final String FILE_PATH = "file_path";
54 public static final String FILE_LENGTH = "file_length";
55 public static final String FILE_RANDOM = "file_random";
56
57 public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + _ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
58 PACK_ID + " TEXT NOT NULL, " +
59 PACK_KEY + " TEXT NOT NULL, " +
60 PACK_TITLE + " TEXT NOT NULL, " +
61 PACK_AUTHOR + " TEXT NOT NULL, " +
62 STICKER_ID + " INTEGER, " +
63 COVER + " INTEGER, " +
64 PACK_ORDER + " INTEGER, " +
65 EMOJI + " TEXT NOT NULL, " +
66 CONTENT_TYPE + " TEXT DEFAULT NULL, " +
67 LAST_USED + " INTEGER, " +
68 INSTALLED + " INTEGER," +
69 FILE_PATH + " TEXT NOT NULL, " +
70 FILE_LENGTH + " INTEGER, " +
71 FILE_RANDOM + " BLOB, " +
72 "UNIQUE(" + PACK_ID + ", " + STICKER_ID + ", " + COVER + ") ON CONFLICT IGNORE)";
73
74 public static final String[] CREATE_INDEXES = {
75 "CREATE INDEX IF NOT EXISTS sticker_pack_id_index ON " + TABLE_NAME + " (" + PACK_ID + ");",
76 "CREATE INDEX IF NOT EXISTS sticker_sticker_id_index ON " + TABLE_NAME + " (" + STICKER_ID + ");"
77 };
78
79 public static final String DIRECTORY = "stickers";
80
81 private final AttachmentSecret attachmentSecret;
82
83 public StickerTable(Context context, SignalDatabase databaseHelper, AttachmentSecret attachmentSecret) {
84 super(context, databaseHelper);
85 this.attachmentSecret = attachmentSecret;
86 }
87
88 public void insertSticker(@NonNull IncomingSticker sticker, @NonNull InputStream dataStream, boolean notify) throws IOException {
89 FileInfo fileInfo = saveStickerImage(dataStream);
90 ContentValues contentValues = new ContentValues();
91
92 contentValues.put(PACK_ID, sticker.getPackId());
93 contentValues.put(PACK_KEY, sticker.getPackKey());
94 contentValues.put(PACK_TITLE, sticker.getPackTitle());
95 contentValues.put(PACK_AUTHOR, sticker.getPackAuthor());
96 contentValues.put(STICKER_ID, sticker.getStickerId());
97 contentValues.put(EMOJI, sticker.getEmoji());
98 contentValues.put(CONTENT_TYPE, sticker.getContentType());
99 contentValues.put(COVER, sticker.isCover() ? 1 : 0);
100 contentValues.put(INSTALLED, sticker.isInstalled() ? 1 : 0);
101 contentValues.put(FILE_PATH, fileInfo.getFile().getAbsolutePath());
102 contentValues.put(FILE_LENGTH, fileInfo.getLength());
103 contentValues.put(FILE_RANDOM, fileInfo.getRandom());
104
105 long id = databaseHelper.getSignalWritableDatabase().insert(TABLE_NAME, null, contentValues);
106 if (id == -1) {
107 String selection = PACK_ID + " = ? AND " + STICKER_ID + " = ? AND " + COVER + " = ?";
108 String[] args = SqlUtil.buildArgs(sticker.getPackId(), sticker.getStickerId(), (sticker.isCover() ? 1 : 0));
109
110 id = databaseHelper.getSignalWritableDatabase().update(TABLE_NAME, contentValues, selection, args);
111 }
112
113 if (id > 0) {
114 notifyStickerListeners();
115
116 if (sticker.isCover()) {
117 notifyStickerPackListeners();
118
119 if (sticker.isInstalled() && notify) {
120 broadcastInstallEvent(sticker.getPackId());
121 }
122 }
123 }
124 }
125
126 public @Nullable StickerRecord getSticker(@NonNull String packId, int stickerId, boolean isCover) {
127 String selection = PACK_ID + " = ? AND " + STICKER_ID + " = ? AND " + COVER + " = ?";
128 String[] args = new String[] { packId, String.valueOf(stickerId), String.valueOf(isCover ? 1 : 0) };
129
130 try (Cursor cursor = databaseHelper.getSignalReadableDatabase().query(TABLE_NAME, null, selection, args, null, null, "1")) {
131 return new StickerRecordReader(cursor).getNext();
132 }
133 }
134
135 public @Nullable StickerPackRecord getStickerPack(@NonNull String packId) {
136 String query = PACK_ID + " = ? AND " + COVER + " = ?";
137 String[] args = new String[] { packId, "1" };
138
139 try (Cursor cursor = databaseHelper.getSignalReadableDatabase().query(TABLE_NAME, null, query, args, null, null, null, "1")) {
140 return new StickerPackRecordReader(cursor).getNext();
141 }
142 }
143
144 public @Nullable Cursor getInstalledStickerPacks() {
145 String selection = COVER + " = ? AND " + INSTALLED + " = ?";
146 String[] args = new String[] { "1", "1" };
147
148 return databaseHelper.getSignalReadableDatabase().query(TABLE_NAME, null, selection, args, null, null, PACK_ORDER + " ASC");
149 }
150
151 public @Nullable Cursor getStickersByEmoji(@NonNull String emoji) {
152 String selection = EMOJI + " LIKE ? AND " + COVER + " = ?";
153 String[] args = new String[] { "%"+emoji+"%", "0" };
154
155 return databaseHelper.getSignalReadableDatabase().query(TABLE_NAME, null, selection, args, null, null, null);
156 }
157
158 public @Nullable Cursor getAllStickerPacks() {
159 return getAllStickerPacks(null);
160 }
161
162 public @Nullable Cursor getAllStickerPacks(@Nullable String limit) {
163 String query = COVER + " = ?";
164 String[] args = new String[] { "1" };
165
166 return databaseHelper.getSignalReadableDatabase().query(TABLE_NAME, null, query, args, null, null, PACK_ORDER + " ASC", limit);
167 }
168
169 public @Nullable Cursor getStickersForPack(@NonNull String packId) {
170 SQLiteDatabase db = databaseHelper.getSignalReadableDatabase();
171 String selection = PACK_ID + " = ? AND " + COVER + " = ?";
172 String[] args = new String[] { packId, "0" };
173
174 return db.query(TABLE_NAME, null, selection, args, null, null, STICKER_ID + " ASC");
175 }
176
177 public @Nullable Cursor getRecentlyUsedStickers(int limit) {
178 SQLiteDatabase db = databaseHelper.getSignalReadableDatabase();
179 String selection = LAST_USED + " > ? AND " + COVER + " = ?";
180 String[] args = new String[] { "0", "0" };
181
182 return db.query(TABLE_NAME, null, selection, args, null, null, LAST_USED + " DESC", String.valueOf(limit));
183 }
184
185 public @NonNull Set<String> getAllStickerFiles() {
186 SQLiteDatabase db = databaseHelper.getSignalReadableDatabase();
187
188 Set<String> files = new HashSet<>();
189 try (Cursor cursor = db.query(TABLE_NAME, new String[] { FILE_PATH }, null, null, null, null, null)) {
190 while (cursor != null && cursor.moveToNext()) {
191 files.add(CursorUtil.requireString(cursor, FILE_PATH));
192 }
193 }
194
195 return files;
196 }
197
198 public @Nullable InputStream getStickerStream(long rowId) throws IOException {
199 String selection = _ID + " = ?";
200 String[] args = new String[] { String.valueOf(rowId) };
201
202 try (Cursor cursor = databaseHelper.getSignalReadableDatabase().query(TABLE_NAME, null, selection, args, null, null, null)) {
203 if (cursor != null && cursor.moveToNext()) {
204 String path = cursor.getString(cursor.getColumnIndexOrThrow(FILE_PATH));
205 byte[] random = cursor.getBlob(cursor.getColumnIndexOrThrow(FILE_RANDOM));
206
207 if (path != null) {
208 return ModernDecryptingPartInputStream.createFor(attachmentSecret, random, new File(path), 0);
209 } else {
210 Log.w(TAG, "getStickerStream("+rowId+") - No sticker data");
211 }
212 } else {
213 Log.i(TAG, "getStickerStream("+rowId+") - Sticker not found.");
214 }
215 }
216
217 return null;
218 }
219
220 public boolean isPackInstalled(@NonNull String packId) {
221 StickerPackRecord record = getStickerPack(packId);
222
223 return (record != null && record.isInstalled());
224 }
225
226 public boolean isPackAvailableAsReference(@NonNull String packId) {
227 return getStickerPack(packId) != null;
228 }
229
230 public void updateStickerLastUsedTime(long rowId, long lastUsed) {
231 String selection = _ID + " = ?";
232 String[] args = new String[] { String.valueOf(rowId) };
233 ContentValues values = new ContentValues();
234
235 values.put(LAST_USED, lastUsed);
236
237 databaseHelper.getSignalWritableDatabase().update(TABLE_NAME, values, selection, args);
238
239 notifyStickerListeners();
240 notifyStickerPackListeners();
241 }
242
243 public void markPackAsInstalled(@NonNull String packKey, boolean notify) {
244 updatePackInstalled(databaseHelper.getSignalWritableDatabase(), packKey, true, notify);
245 notifyStickerPackListeners();
246 }
247
248 public void deleteOrphanedPacks() {
249 SQLiteDatabase db = databaseHelper.getSignalWritableDatabase();
250 String query = "SELECT " + PACK_ID + " FROM " + TABLE_NAME + " WHERE " + INSTALLED + " = ? AND " +
251 PACK_ID + " NOT IN (" +
252 "SELECT DISTINCT " + AttachmentTable.STICKER_PACK_ID + " FROM " + AttachmentTable.TABLE_NAME + " " +
253 "WHERE " + AttachmentTable.STICKER_PACK_ID + " NOT NULL" +
254 ")";
255 String[] args = new String[] { "0" };
256
257 db.beginTransaction();
258
259 try {
260 boolean performedDelete = false;
261
262 try (Cursor cursor = db.rawQuery(query, args)) {
263 while (cursor != null && cursor.moveToNext()) {
264 String packId = cursor.getString(cursor.getColumnIndexOrThrow(PACK_ID));
265
266 if (!BlessedPacks.contains(packId)) {
267 deletePack(db, packId);
268 performedDelete = true;
269 }
270 }
271 }
272
273 db.setTransactionSuccessful();
274
275 if (performedDelete) {
276 notifyStickerPackListeners();
277 notifyStickerListeners();
278 }
279 } finally {
280 db.endTransaction();
281 }
282 }
283
284 public void uninstallPack(@NonNull String packId) {
285 SQLiteDatabase db = databaseHelper.getSignalWritableDatabase();
286
287 db.beginTransaction();
288 try {
289 updatePackInstalled(db, packId, false, false);
290 deleteStickersInPackExceptCover(db, packId);
291
292 db.setTransactionSuccessful();
293 notifyStickerPackListeners();
294 notifyStickerListeners();
295 } finally {
296 db.endTransaction();
297 }
298 }
299
300 public void updatePackOrder(@NonNull List<StickerPackRecord> packsInOrder) {
301 SQLiteDatabase db = databaseHelper.getSignalWritableDatabase();
302
303 db.beginTransaction();
304 try {
305 String selection = PACK_ID + " = ? AND " + COVER + " = ?";
306
307 for (int i = 0; i < packsInOrder.size(); i++) {
308 String[] args = new String[]{ packsInOrder.get(i).getPackId(), "1" };
309 ContentValues values = new ContentValues();
310
311 values.put(PACK_ORDER, i);
312
313 db.update(TABLE_NAME, values, selection, args);
314 }
315
316 db.setTransactionSuccessful();
317 notifyStickerPackListeners();
318 } finally {
319 db.endTransaction();
320 }
321 }
322
323 private void updatePackInstalled(@NonNull SQLiteDatabase db, @NonNull String packId, boolean installed, boolean notify) {
324 StickerPackRecord existing = getStickerPack(packId);
325
326 if (existing != null && existing.isInstalled() == installed) {
327 return;
328 }
329
330 String selection = PACK_ID + " = ?";
331 String[] args = new String[]{ packId };
332 ContentValues values = new ContentValues(1);
333
334 values.put(INSTALLED, installed ? 1 : 0);
335 db.update(TABLE_NAME, values, selection, args);
336
337 if (installed && notify) {
338 broadcastInstallEvent(packId);
339 }
340 }
341
342 private FileInfo saveStickerImage(@NonNull InputStream inputStream) throws IOException {
343 File partsDirectory = context.getDir(DIRECTORY, Context.MODE_PRIVATE);
344 File file = File.createTempFile("sticker", ".mms", partsDirectory);
345 Pair<byte[], OutputStream> out = ModernEncryptingPartOutputStream.createFor(attachmentSecret, file, false);
346 long length = StreamUtil.copy(inputStream, out.second);
347
348 return new FileInfo(file, length, out.first);
349 }
350
351 private void deleteSticker(@NonNull SQLiteDatabase db, long rowId, @Nullable String filePath) {
352 String selection = _ID + " = ?";
353 String[] args = new String[] { String.valueOf(rowId) };
354
355 db.delete(TABLE_NAME, selection, args);
356
357 if (!TextUtils.isEmpty(filePath)) {
358 new File(filePath).delete();
359 }
360 }
361
362 private void deletePack(@NonNull SQLiteDatabase db, @NonNull String packId) {
363 String selection = PACK_ID + " = ?";
364 String[] args = new String[] { packId };
365
366 db.delete(TABLE_NAME, selection, args);
367
368 deleteStickersInPack(db, packId);
369 }
370
371 private void deleteStickersInPack(@NonNull SQLiteDatabase db, @NonNull String packId) {
372 String selection = PACK_ID + " = ?";
373 String[] args = new String[] { packId };
374
375 db.beginTransaction();
376
377 try {
378 try (Cursor cursor = db.query(TABLE_NAME, null, selection, args, null, null, null)) {
379 while (cursor != null && cursor.moveToNext()) {
380 String filePath = cursor.getString(cursor.getColumnIndexOrThrow(FILE_PATH));
381 long rowId = cursor.getLong(cursor.getColumnIndexOrThrow(_ID));
382
383 deleteSticker(db, rowId, filePath);
384 }
385 }
386
387 db.setTransactionSuccessful();
388 } finally {
389 db.endTransaction();
390 }
391
392 db.delete(TABLE_NAME, selection, args);
393 }
394
395 private void deleteStickersInPackExceptCover(@NonNull SQLiteDatabase db, @NonNull String packId) {
396 String selection = PACK_ID + " = ? AND " + COVER + " = ?";
397 String[] args = new String[] { packId, "0" };
398
399 db.beginTransaction();
400
401 try {
402 try (Cursor cursor = db.query(TABLE_NAME, null, selection, args, null, null, null)) {
403 while (cursor != null && cursor.moveToNext()) {
404 long rowId = cursor.getLong(cursor.getColumnIndexOrThrow(_ID));
405 String filePath = cursor.getString(cursor.getColumnIndexOrThrow(FILE_PATH));
406
407 deleteSticker(db, rowId, filePath);
408 }
409 }
410
411 db.setTransactionSuccessful();
412 } finally {
413 db.endTransaction();
414 }
415 }
416
417 private void broadcastInstallEvent(@NonNull String packId) {
418 StickerPackRecord pack = getStickerPack(packId);
419
420 if (pack != null) {
421 EventBus.getDefault().postSticky(new StickerPackInstallEvent(new DecryptableUri(pack.getCover().getUri())));
422 }
423 }
424
425 private static final class FileInfo {
426 private final File file;
427 private final long length;
428 private final byte[] random;
429
430 private FileInfo(@NonNull File file, long length, @NonNull byte[] random) {
431 this.file = file;
432 this.length = length;
433 this.random = random;
434 }
435
436 public File getFile() {
437 return file;
438 }
439
440 public long getLength() {
441 return length;
442 }
443
444 public byte[] getRandom() {
445 return random;
446 }
447 }
448
449 public static final class StickerRecordReader implements Closeable {
450
451 private final Cursor cursor;
452
453 public StickerRecordReader(@Nullable Cursor cursor) {
454 this.cursor = cursor;
455 }
456
457 public @Nullable StickerRecord getNext() {
458 if (cursor == null || !cursor.moveToNext()) {
459 return null;
460 }
461
462 return getCurrent();
463 }
464
465 public @NonNull StickerRecord getCurrent() {
466 return new StickerRecord(cursor.getLong(cursor.getColumnIndexOrThrow(_ID)),
467 cursor.getString(cursor.getColumnIndexOrThrow(PACK_ID)),
468 cursor.getString(cursor.getColumnIndexOrThrow(PACK_KEY)),
469 cursor.getInt(cursor.getColumnIndexOrThrow(STICKER_ID)),
470 cursor.getString(cursor.getColumnIndexOrThrow(EMOJI)),
471 cursor.getString(cursor.getColumnIndexOrThrow(CONTENT_TYPE)),
472 cursor.getLong(cursor.getColumnIndexOrThrow(FILE_LENGTH)),
473 cursor.getInt(cursor.getColumnIndexOrThrow(COVER)) == 1);
474 }
475
476 @Override
477 public void close() {
478 if (cursor != null) {
479 cursor.close();
480 }
481 }
482 }
483
484 public static final class StickerPackRecordReader implements Closeable {
485
486 private final Cursor cursor;
487
488 public StickerPackRecordReader(@Nullable Cursor cursor) {
489 this.cursor = cursor;
490 }
491
492 public @Nullable StickerPackRecord getNext() {
493 if (cursor == null || !cursor.moveToNext()) {
494 return null;
495 }
496
497 return getCurrent();
498 }
499
500 public @NonNull StickerPackRecord getCurrent() {
501 StickerRecord cover = new StickerRecordReader(cursor).getCurrent();
502
503 return new StickerPackRecord(cursor.getString(cursor.getColumnIndexOrThrow(PACK_ID)),
504 cursor.getString(cursor.getColumnIndexOrThrow(PACK_KEY)),
505 cursor.getString(cursor.getColumnIndexOrThrow(PACK_TITLE)),
506 cursor.getString(cursor.getColumnIndexOrThrow(PACK_AUTHOR)),
507 cover,
508 cursor.getInt(cursor.getColumnIndexOrThrow(INSTALLED)) == 1);
509 }
510
511 @Override
512 public void close() {
513 if (cursor != null) {
514 cursor.close();
515 }
516 }
517 }
518}