aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-android/app/src/main/kotlin/org/meshbay/client/drive/DriveKind.kt
blob: 69a658a33724134a74f93c619df3add940dad5fe (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
package org.meshbay.client.drive

import org.json.JSONObject
import java.util.Locale

/**
 * A backup of folders chosen through the system's picker: the files backup,
 * or WhatsApp's own folder (docs/MESHBAY_DESIGN.md §9.13). One class runs both
 * (DriveChannels); this is what differs.
 */
data class DriveKind(
    /** Names the channels (`drivesync:status`) and the preferences. */
    val prefix: String,
    /** The folder under the destination: `<account>-<suffix>`. */
    val suffix: String,
    val notifyId: Int,
    /** Why a chosen folder is refused, from its tree document id, or null. */
    val refusal: (String) -> String?,
    /** One folder only: choosing again replaces it. */
    val single: Boolean,
    /** The chosen folder's own name as the first level on the node. */
    val keepTop: Boolean,
    /** Whether a file found there is sent, given the options the person set. */
    val include: (DriveFile, JSONObject) -> Boolean,
    /** The files a restore needs together, listed in the manifest as one set; null when none. */
    val restoreSet: ((DriveFile) -> Boolean)?,
    /** Where the picker opens. */
    val initialDocId: String?,
) {
    val path: String get() = "/$prefix/"

    companion object {
        val FILES = DriveKind(
            prefix = "drivesync", suffix = "drive", notifyId = 13,
            refusal = DrivePlan::refusal, single = false, keepTop = true,
            include = { _, _ -> true }, restoreSet = null, initialDocId = null,
        )

        val WHATSAPP = DriveKind(
            prefix = "wasync", suffix = "whatsapp", notifyId = 14,
            refusal = WhatsApp::refusal, single = true, keepTop = false,
            include = { f, options -> WhatsApp.include(f, options.optBoolean("media", false)) },
            restoreSet = WhatsApp::inRestoreSet,
            initialDocId = "primary:" + WhatsApp.FOLDER,
        )
    }
}

/**
 * WhatsApp's folder, `Android/media/com.whatsapp/WhatsApp` (or the business
 * application's): what a restore onto a new phone needs, and the media.
 *
 * `Databases/` holds the chats, encrypted by WhatsApp with a key only it (or
 * the person's end-to-end backup password) opens: a full copy written weekly,
 * `msgstore.db.crypt14`, and an increment a night,
 * `msgstore-increment-N.db.crypt14`. When a new week starts WhatsApp renames
 * the last week's set with its date; those dated copies are what this backup
 * already kept as earlier versions, so they are not sent again. `Backups/`
 * holds the rest of a restore (contacts, settings, stickers).
 */
object WhatsApp {
    const val FOLDER = "Android/media/com.whatsapp/WhatsApp"
    private val FOLDERS = setOf(FOLDER, "Android/media/com.whatsapp.w4b/WhatsApp Business")
    private val DATED = Regex("^msgstore(-increment-\\d+)?-\\d{4}-\\d{2}-\\d{2}\\..*", RegexOption.IGNORE_CASE)

    fun refusal(treeDocId: String): String? =
        if (DrivePlan.pathOf(treeDocId) in FOLDERS) null else "not_whatsapp"

    private fun top(f: DriveFile) = f.sub.substringBefore('/').lowercase(Locale.ROOT)

    fun include(f: DriveFile, media: Boolean): Boolean = when (top(f)) {
        "databases" -> !DATED.matches(f.name)
        "backups" -> true
        "media" -> media
        else -> false
    }

    fun inRestoreSet(f: DriveFile): Boolean = include(f, false)
}