mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-06 11:48:24 +00:00
refactor(commons): move the marmot encrypted stores and the NIP-11 / online caches
Two clusters from the survey of what was left in amethyst/, both cases of code that was shared-layer already and just happened to live in the app. The four Marmot stores go to commons/jvmAndroid/marmot, next to the EncryptedAppendLog they were already built on. Nothing in them was Android despite the prefix: no android.* import, no Context, just (rootDir: File, encryption: SecretEncryption) and the quartz interfaces. One call site, AccountCacheState. The KDoc lines claiming "Android implementation" and "AES/GCM via Android KeyStore" are reworded: SecretEncryption is expect/actual, AndroidKeyStore on Android and a key file on desktop, so the old wording is now only half true. Commons already referred to two of them by name in its own KDoc; those references are updated. They are Encrypted* rather than File*, and that is not cosmetic. cli/stores already declares FileMlsGroupStateStore, FileMarmotMessageStore, FileKeyPackageBundleStore and FilePublishObligationStore — deliberately UNENCRYPTED test-harness stores, in a module that depends on commons. Two same-named classes with opposite encryption semantics, one import away from each other, is how MLS state ends up written in plaintext. Encrypted* is also what these actually are, and reads correctly next to EncryptedAppendLog. Nip11CachedRetriever + Nip11Retriever + RetrieveResult go to commons/relays/nip11, and OnlineChecker to commons/service. RetrieveResult had to travel: the cache is typed on it and commons cannot import from amethyst. Nip11RetrieverTest travels too — it sat in the same package and used the class with no import line. LoadRelayInfo and RelaySupportsNip stay behind; both reach Amethyst.instance. android.util.LruCache -> androidx.collection.LruCache is not the pure import swap it looks like, and the compiler said so: androidx bounds V to Any, while android.util.LruCache is a Java platform type that accepted LruCache<NormalizedRelayUrl, RetrieveResult?>. Checked all four put() sites first — every one stores a concrete RetrieveResult, so the nullable argument never meant anything and get() still returns null on a miss, which is what the readers already branch on. Dropping it is behaviour-preserving. NotifyCoordinator does NOT move despite being grouped with the other two: android.util.LruCache really is its only platform import, but it takes accountForPubkey: (HexKey) -> Account?, and Account lives in amethyst/model. It needs that abstraction, not an import swap. 17 new tests. OnlineChecker's predicates decide whether the UI shows a player or an offline placeholder, so the five-minute TTL is pinned in both directions — a stale online entry must stop reading online, and a stale offline one must stop suppressing retries — along with resetIfOfflineToRetry dropping only failures, since evicting good entries would refetch every URL that already worked. Its suspend probe is left alone: it needs a real OkHttp round trip and commons has no MockWebServer, so there is no honest way to drive it. Same reason Nip11CachedRetriever gets nothing new here; its fetch and error-caching paths are all network. The Marmot tests cover what restart depends on: group state, sender ratchet and message log surviving a new store over the same directory, deletes removing both the state and the listing, and two account directories not seeing each other. Writing them found validation I had not noticed reading the code — group ids must be hex, which is a path-traversal guard — after a first pass using readable labels failed every test on it. That guard is pinned now too: "../escape" and friends are rejected rather than resolved to a path. Verified: :commons:jvmTest (2549, 0 failures), :amethyst:testPlayDebugUnitTest, :cli:compileKotlin, :commons:verifyKmpPurity, :commons:compileCommonMainKotlinMetadata, :amethyst:compilePlayDebugKotlin. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AXvKXakvup4inNFfAhhr4L
This commit is contained in:
@@ -55,6 +55,7 @@ import com.vitorpamplona.amethyst.commons.relayClient.event.EventFinderQueryStat
|
|||||||
import com.vitorpamplona.amethyst.commons.relayClient.speedLogger.RelaySpeedLogger
|
import com.vitorpamplona.amethyst.commons.relayClient.speedLogger.RelaySpeedLogger
|
||||||
import com.vitorpamplona.amethyst.commons.relayClient.user.UserFinderQueryState
|
import com.vitorpamplona.amethyst.commons.relayClient.user.UserFinderQueryState
|
||||||
import com.vitorpamplona.amethyst.commons.relays.health.TorCircuitHealthTracker
|
import com.vitorpamplona.amethyst.commons.relays.health.TorCircuitHealthTracker
|
||||||
|
import com.vitorpamplona.amethyst.commons.relays.nip11.Nip11CachedRetriever
|
||||||
import com.vitorpamplona.amethyst.commons.richtext.CachedAsciiDocToMarkdown
|
import com.vitorpamplona.amethyst.commons.richtext.CachedAsciiDocToMarkdown
|
||||||
import com.vitorpamplona.amethyst.commons.richtext.CachedRichTextParser
|
import com.vitorpamplona.amethyst.commons.richtext.CachedRichTextParser
|
||||||
import com.vitorpamplona.amethyst.commons.robohash.CachedRobohash
|
import com.vitorpamplona.amethyst.commons.robohash.CachedRobohash
|
||||||
@@ -75,7 +76,6 @@ import com.vitorpamplona.amethyst.commons.state.UiSettingsState
|
|||||||
import com.vitorpamplona.amethyst.commons.tor.TorSettings
|
import com.vitorpamplona.amethyst.commons.tor.TorSettings
|
||||||
import com.vitorpamplona.amethyst.model.Account
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
import com.vitorpamplona.amethyst.model.accountsCache.AccountCacheState
|
import com.vitorpamplona.amethyst.model.accountsCache.AccountCacheState
|
||||||
import com.vitorpamplona.amethyst.model.nip11RelayInfo.Nip11CachedRetriever
|
|
||||||
import com.vitorpamplona.amethyst.model.nip60Cashu.CashuPreferences
|
import com.vitorpamplona.amethyst.model.nip60Cashu.CashuPreferences
|
||||||
import com.vitorpamplona.amethyst.model.preferences.UiSharedPreferences
|
import com.vitorpamplona.amethyst.model.preferences.UiSharedPreferences
|
||||||
import com.vitorpamplona.amethyst.model.privacyOptions.RoleBasedHttpClientBuilder
|
import com.vitorpamplona.amethyst.model.privacyOptions.RoleBasedHttpClientBuilder
|
||||||
|
|||||||
+13
-13
@@ -26,6 +26,10 @@ import com.vitorpamplona.amethyst.commons.connectedApps.nip46.InMemoryNip46Clien
|
|||||||
import com.vitorpamplona.amethyst.commons.connectedApps.nip46.Nip46ClientStore
|
import com.vitorpamplona.amethyst.commons.connectedApps.nip46.Nip46ClientStore
|
||||||
import com.vitorpamplona.amethyst.commons.connectedApps.signers.InMemoryNostrSignerPermissionStore
|
import com.vitorpamplona.amethyst.commons.connectedApps.signers.InMemoryNostrSignerPermissionStore
|
||||||
import com.vitorpamplona.amethyst.commons.connectedApps.signers.NostrSignerPermissionStore
|
import com.vitorpamplona.amethyst.commons.connectedApps.signers.NostrSignerPermissionStore
|
||||||
|
import com.vitorpamplona.amethyst.commons.marmot.EncryptedKeyPackageBundleStore
|
||||||
|
import com.vitorpamplona.amethyst.commons.marmot.EncryptedMarmotMessageStore
|
||||||
|
import com.vitorpamplona.amethyst.commons.marmot.EncryptedMlsGroupStateStore
|
||||||
|
import com.vitorpamplona.amethyst.commons.marmot.EncryptedPublishObligationStore
|
||||||
import com.vitorpamplona.amethyst.commons.marmot.InMemoryMlsGroupStateStore
|
import com.vitorpamplona.amethyst.commons.marmot.InMemoryMlsGroupStateStore
|
||||||
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
|
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
|
||||||
import com.vitorpamplona.amethyst.commons.model.marmot.AndroidIngestDedupStore
|
import com.vitorpamplona.amethyst.commons.model.marmot.AndroidIngestDedupStore
|
||||||
@@ -36,10 +40,6 @@ import com.vitorpamplona.amethyst.commons.relayauth.DataStoreRelayAuthPermission
|
|||||||
import com.vitorpamplona.amethyst.commons.service.pow.PoWPublishQueue
|
import com.vitorpamplona.amethyst.commons.service.pow.PoWPublishQueue
|
||||||
import com.vitorpamplona.amethyst.model.Account
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||||
import com.vitorpamplona.amethyst.model.marmot.AndroidKeyPackageBundleStore
|
|
||||||
import com.vitorpamplona.amethyst.model.marmot.AndroidMarmotMessageStore
|
|
||||||
import com.vitorpamplona.amethyst.model.marmot.AndroidMlsGroupStateStore
|
|
||||||
import com.vitorpamplona.amethyst.model.marmot.AndroidPublishObligationStore
|
|
||||||
import com.vitorpamplona.amethyst.service.location.LocationState
|
import com.vitorpamplona.amethyst.service.location.LocationState
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
||||||
@@ -250,13 +250,13 @@ class AccountCacheState(
|
|||||||
val mlsStore =
|
val mlsStore =
|
||||||
try {
|
try {
|
||||||
Log.d("AccountCacheState") {
|
Log.d("AccountCacheState") {
|
||||||
"Initializing AndroidMlsGroupStateStore for ${signer.pubKey.take(8)}… at ${accountDir.absolutePath}"
|
"Initializing EncryptedMlsGroupStateStore for ${signer.pubKey.take(8)}… at ${accountDir.absolutePath}"
|
||||||
}
|
}
|
||||||
AndroidMlsGroupStateStore(accountDir)
|
EncryptedMlsGroupStateStore(accountDir)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(
|
Log.e(
|
||||||
"AccountCacheState",
|
"AccountCacheState",
|
||||||
"Failed to initialize AndroidMlsGroupStateStore, falling back to in-memory store (Marmot groups will NOT persist across restarts)",
|
"Failed to initialize EncryptedMlsGroupStateStore, falling back to in-memory store (Marmot groups will NOT persist across restarts)",
|
||||||
e,
|
e,
|
||||||
)
|
)
|
||||||
InMemoryMlsGroupStateStore()
|
InMemoryMlsGroupStateStore()
|
||||||
@@ -267,11 +267,11 @@ class AccountCacheState(
|
|||||||
|
|
||||||
val marmotMessageStore =
|
val marmotMessageStore =
|
||||||
try {
|
try {
|
||||||
AndroidMarmotMessageStore(accountDir)
|
EncryptedMarmotMessageStore(accountDir)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(
|
Log.e(
|
||||||
"AccountCacheState",
|
"AccountCacheState",
|
||||||
"Failed to initialize AndroidMarmotMessageStore (Marmot messages will NOT persist across restarts)",
|
"Failed to initialize EncryptedMarmotMessageStore (Marmot messages will NOT persist across restarts)",
|
||||||
e,
|
e,
|
||||||
)
|
)
|
||||||
null
|
null
|
||||||
@@ -279,11 +279,11 @@ class AccountCacheState(
|
|||||||
|
|
||||||
val marmotKeyPackageStore =
|
val marmotKeyPackageStore =
|
||||||
try {
|
try {
|
||||||
AndroidKeyPackageBundleStore(accountDir)
|
EncryptedKeyPackageBundleStore(accountDir)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(
|
Log.e(
|
||||||
"AccountCacheState",
|
"AccountCacheState",
|
||||||
"Failed to initialize AndroidKeyPackageBundleStore (Marmot KeyPackages will NOT persist across restarts)",
|
"Failed to initialize EncryptedKeyPackageBundleStore (Marmot KeyPackages will NOT persist across restarts)",
|
||||||
e,
|
e,
|
||||||
)
|
)
|
||||||
null
|
null
|
||||||
@@ -291,11 +291,11 @@ class AccountCacheState(
|
|||||||
|
|
||||||
val marmotPublishObligationStore =
|
val marmotPublishObligationStore =
|
||||||
try {
|
try {
|
||||||
AndroidPublishObligationStore(accountDir)
|
EncryptedPublishObligationStore(accountDir)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(
|
Log.e(
|
||||||
"AccountCacheState",
|
"AccountCacheState",
|
||||||
"Failed to initialize AndroidPublishObligationStore " +
|
"Failed to initialize EncryptedPublishObligationStore " +
|
||||||
"(a Marmot commit interrupted mid-publish will NOT be retried after a restart)",
|
"(a Marmot commit interrupted mid-publish will NOT be retried after a restart)",
|
||||||
e,
|
e,
|
||||||
)
|
)
|
||||||
|
|||||||
+1
@@ -25,6 +25,7 @@ import androidx.compose.runtime.LaunchedEffect
|
|||||||
import androidx.compose.runtime.State
|
import androidx.compose.runtime.State
|
||||||
import androidx.compose.runtime.produceState
|
import androidx.compose.runtime.produceState
|
||||||
import com.vitorpamplona.amethyst.Amethyst
|
import com.vitorpamplona.amethyst.Amethyst
|
||||||
|
import com.vitorpamplona.amethyst.commons.relays.nip11.Nip11CachedRetriever
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||||
import com.vitorpamplona.quartz.nip11RelayInfo.Nip11RelayInformation
|
import com.vitorpamplona.quartz.nip11RelayInfo.Nip11RelayInformation
|
||||||
import com.vitorpamplona.quartz.utils.Log
|
import com.vitorpamplona.quartz.utils.Log
|
||||||
|
|||||||
+1
-1
@@ -96,6 +96,7 @@ import com.vitorpamplona.amethyst.commons.resources.unauthorized_exception
|
|||||||
import com.vitorpamplona.amethyst.commons.resources.unauthorized_exception_description
|
import com.vitorpamplona.amethyst.commons.resources.unauthorized_exception_description
|
||||||
import com.vitorpamplona.amethyst.commons.resources.user_x_does_not_have_a_lightning_address_setup_to_receive_sats
|
import com.vitorpamplona.amethyst.commons.resources.user_x_does_not_have_a_lightning_address_setup_to_receive_sats
|
||||||
import com.vitorpamplona.amethyst.commons.resources.video_saved_to_the_gallery
|
import com.vitorpamplona.amethyst.commons.resources.video_saved_to_the_gallery
|
||||||
|
import com.vitorpamplona.amethyst.commons.service.OnlineChecker
|
||||||
import com.vitorpamplona.amethyst.commons.service.broadcast.BroadcastTracker
|
import com.vitorpamplona.amethyst.commons.service.broadcast.BroadcastTracker
|
||||||
import com.vitorpamplona.amethyst.commons.service.http.EmptyRoleBasedHttpClientBuilder
|
import com.vitorpamplona.amethyst.commons.service.http.EmptyRoleBasedHttpClientBuilder
|
||||||
import com.vitorpamplona.amethyst.commons.service.http.IRoleBasedHttpClientBuilder
|
import com.vitorpamplona.amethyst.commons.service.http.IRoleBasedHttpClientBuilder
|
||||||
@@ -116,7 +117,6 @@ import com.vitorpamplona.amethyst.model.LatestKeyPackageOwner
|
|||||||
import com.vitorpamplona.amethyst.model.UrlCachedPreviewer
|
import com.vitorpamplona.amethyst.model.UrlCachedPreviewer
|
||||||
import com.vitorpamplona.amethyst.model.privacyOptions.RoleBasedHttpClientBuilder
|
import com.vitorpamplona.amethyst.model.privacyOptions.RoleBasedHttpClientBuilder
|
||||||
import com.vitorpamplona.amethyst.service.ClinkDebitPayer
|
import com.vitorpamplona.amethyst.service.ClinkDebitPayer
|
||||||
import com.vitorpamplona.amethyst.service.OnlineChecker
|
|
||||||
import com.vitorpamplona.amethyst.service.V4VPaymentHandler
|
import com.vitorpamplona.amethyst.service.V4VPaymentHandler
|
||||||
import com.vitorpamplona.amethyst.service.ZapPaymentHandler
|
import com.vitorpamplona.amethyst.service.ZapPaymentHandler
|
||||||
import com.vitorpamplona.amethyst.service.cashu.melt.MeltProcessor
|
import com.vitorpamplona.amethyst.service.cashu.melt.MeltProcessor
|
||||||
|
|||||||
+1
-1
@@ -27,8 +27,8 @@ import androidx.compose.runtime.LaunchedEffect
|
|||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
|
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
|
||||||
|
import com.vitorpamplona.amethyst.commons.service.OnlineChecker
|
||||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||||
import com.vitorpamplona.amethyst.service.OnlineChecker
|
|
||||||
import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold
|
import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold
|
||||||
import com.vitorpamplona.amethyst.ui.note.LoadLiveActivityChannel
|
import com.vitorpamplona.amethyst.ui.note.LoadLiveActivityChannel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
|||||||
+1
-1
@@ -32,8 +32,8 @@ import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.author.Aut
|
|||||||
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.community.SingleCommunityTopNavFilter
|
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.community.SingleCommunityTopNavFilter
|
||||||
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavFilter
|
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavFilter
|
||||||
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.muted.MutedAuthorsByProxyTopNavFilter
|
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.muted.MutedAuthorsByProxyTopNavFilter
|
||||||
|
import com.vitorpamplona.amethyst.commons.service.OnlineChecker
|
||||||
import com.vitorpamplona.amethyst.model.Account
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
import com.vitorpamplona.amethyst.service.OnlineChecker
|
|
||||||
import com.vitorpamplona.amethyst.ui.dal.FilterByListParams
|
import com.vitorpamplona.amethyst.ui.dal.FilterByListParams
|
||||||
import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingRoomEvent
|
import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingRoomEvent
|
||||||
import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingSpaceEvent
|
import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingSpaceEvent
|
||||||
|
|||||||
+1
-1
@@ -74,6 +74,7 @@ import com.vitorpamplona.amethyst.commons.resources.feed_is_empty
|
|||||||
import com.vitorpamplona.amethyst.commons.resources.home_tab_everything
|
import com.vitorpamplona.amethyst.commons.resources.home_tab_everything
|
||||||
import com.vitorpamplona.amethyst.commons.resources.new_threads
|
import com.vitorpamplona.amethyst.commons.resources.new_threads
|
||||||
import com.vitorpamplona.amethyst.commons.resources.refresh
|
import com.vitorpamplona.amethyst.commons.resources.refresh
|
||||||
|
import com.vitorpamplona.amethyst.commons.service.OnlineChecker
|
||||||
import com.vitorpamplona.amethyst.commons.ui.components.CrossfadeIfEnabled
|
import com.vitorpamplona.amethyst.commons.ui.components.CrossfadeIfEnabled
|
||||||
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedError
|
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedError
|
||||||
import com.vitorpamplona.amethyst.commons.ui.feeds.LoadingFeed
|
import com.vitorpamplona.amethyst.commons.ui.feeds.LoadingFeed
|
||||||
@@ -94,7 +95,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size5dp
|
|||||||
import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer
|
import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer
|
||||||
import com.vitorpamplona.amethyst.commons.ui.theme.TabRowHeight
|
import com.vitorpamplona.amethyst.commons.ui.theme.TabRowHeight
|
||||||
import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonRow
|
import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonRow
|
||||||
import com.vitorpamplona.amethyst.service.OnlineChecker
|
|
||||||
import com.vitorpamplona.amethyst.service.location.LocationState
|
import com.vitorpamplona.amethyst.service.location.LocationState
|
||||||
import com.vitorpamplona.amethyst.ui.feeds.ChannelFeedContentState
|
import com.vitorpamplona.amethyst.ui.feeds.ChannelFeedContentState
|
||||||
import com.vitorpamplona.amethyst.ui.feeds.ChannelFeedState
|
import com.vitorpamplona.amethyst.ui.feeds.ChannelFeedState
|
||||||
|
|||||||
+1
-1
@@ -32,7 +32,7 @@ import com.vitorpamplona.amethyst.commons.model.Channel
|
|||||||
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
|
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
|
||||||
import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel
|
import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel
|
||||||
import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel
|
import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel
|
||||||
import com.vitorpamplona.amethyst.service.OnlineChecker
|
import com.vitorpamplona.amethyst.commons.service.OnlineChecker
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingSpaceEvent
|
import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingSpaceEvent
|
||||||
import com.vitorpamplona.quartz.utils.Log
|
import com.vitorpamplona.quartz.utils.Log
|
||||||
|
|||||||
+1
-1
@@ -32,8 +32,8 @@ import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.author.Aut
|
|||||||
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.community.SingleCommunityTopNavFilter
|
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.community.SingleCommunityTopNavFilter
|
||||||
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavFilter
|
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavFilter
|
||||||
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.muted.MutedAuthorsByProxyTopNavFilter
|
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.muted.MutedAuthorsByProxyTopNavFilter
|
||||||
|
import com.vitorpamplona.amethyst.commons.service.OnlineChecker
|
||||||
import com.vitorpamplona.amethyst.model.Account
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
import com.vitorpamplona.amethyst.service.OnlineChecker
|
|
||||||
import com.vitorpamplona.amethyst.ui.dal.FilterByListParams
|
import com.vitorpamplona.amethyst.ui.dal.FilterByListParams
|
||||||
import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingRoomEvent
|
import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingRoomEvent
|
||||||
import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingSpaceEvent
|
import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingSpaceEvent
|
||||||
|
|||||||
+1
-1
@@ -41,6 +41,7 @@ import androidx.compose.ui.platform.LocalClipboard
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
||||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||||
|
import com.vitorpamplona.amethyst.commons.relays.nip11.Nip11CachedRetriever
|
||||||
import com.vitorpamplona.amethyst.commons.relays.ui.RelayCountResult
|
import com.vitorpamplona.amethyst.commons.relays.ui.RelayCountResult
|
||||||
import com.vitorpamplona.amethyst.commons.relays.ui.RelayDragState
|
import com.vitorpamplona.amethyst.commons.relays.ui.RelayDragState
|
||||||
import com.vitorpamplona.amethyst.commons.relays.ui.RelayEventCountRow
|
import com.vitorpamplona.amethyst.commons.relays.ui.RelayEventCountRow
|
||||||
@@ -59,7 +60,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Height25Modifier
|
|||||||
import com.vitorpamplona.amethyst.commons.ui.theme.LargeRelayIconModifier
|
import com.vitorpamplona.amethyst.commons.ui.theme.LargeRelayIconModifier
|
||||||
import com.vitorpamplona.amethyst.commons.ui.theme.ReactionRowHeightChatMaxWidth
|
import com.vitorpamplona.amethyst.commons.ui.theme.ReactionRowHeightChatMaxWidth
|
||||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size25dp
|
import com.vitorpamplona.amethyst.commons.ui.theme.Size25dp
|
||||||
import com.vitorpamplona.amethyst.model.nip11RelayInfo.Nip11CachedRetriever
|
|
||||||
import com.vitorpamplona.amethyst.model.nip11RelayInfo.loadRelayInfo
|
import com.vitorpamplona.amethyst.model.nip11RelayInfo.loadRelayInfo
|
||||||
import com.vitorpamplona.amethyst.ui.note.RenderRelayIcon
|
import com.vitorpamplona.amethyst.ui.note.RenderRelayIcon
|
||||||
import com.vitorpamplona.amethyst.ui.note.UserPicture
|
import com.vitorpamplona.amethyst.ui.note.UserPicture
|
||||||
|
|||||||
+1
-1
@@ -23,11 +23,11 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import com.vitorpamplona.amethyst.commons.model.navigation.Route
|
import com.vitorpamplona.amethyst.commons.model.navigation.Route
|
||||||
|
import com.vitorpamplona.amethyst.commons.relays.nip11.Nip11CachedRetriever
|
||||||
import com.vitorpamplona.amethyst.commons.relays.ui.RelayCountResult
|
import com.vitorpamplona.amethyst.commons.relays.ui.RelayCountResult
|
||||||
import com.vitorpamplona.amethyst.commons.relays.ui.RelayDragState
|
import com.vitorpamplona.amethyst.commons.relays.ui.RelayDragState
|
||||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||||
import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings
|
import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings
|
||||||
import com.vitorpamplona.amethyst.model.nip11RelayInfo.Nip11CachedRetriever
|
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
+1
-1
@@ -45,6 +45,7 @@ import androidx.compose.ui.text.input.KeyboardType
|
|||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.vitorpamplona.amethyst.Amethyst
|
import com.vitorpamplona.amethyst.Amethyst
|
||||||
|
import com.vitorpamplona.amethyst.commons.relays.nip11.Nip11CachedRetriever
|
||||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||||
import com.vitorpamplona.amethyst.commons.resources.add
|
import com.vitorpamplona.amethyst.commons.resources.add
|
||||||
import com.vitorpamplona.amethyst.commons.resources.add_a_relay
|
import com.vitorpamplona.amethyst.commons.resources.add_a_relay
|
||||||
@@ -58,7 +59,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.PopupUpEffect
|
|||||||
import com.vitorpamplona.amethyst.commons.ui.theme.StdEndPadding
|
import com.vitorpamplona.amethyst.commons.ui.theme.StdEndPadding
|
||||||
import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn
|
import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn
|
||||||
import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText
|
import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText
|
||||||
import com.vitorpamplona.amethyst.model.nip11RelayInfo.Nip11CachedRetriever
|
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.client.stats.RelayStat
|
import com.vitorpamplona.quartz.nip01Core.relay.client.stats.RelayStat
|
||||||
|
|||||||
+1
-1
@@ -26,11 +26,11 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import com.vitorpamplona.amethyst.commons.relays.nip11.Nip11CachedRetriever
|
||||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||||
import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings
|
import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings
|
||||||
import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness
|
import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness
|
||||||
import com.vitorpamplona.amethyst.commons.ui.theme.HalfVertPadding
|
import com.vitorpamplona.amethyst.commons.ui.theme.HalfVertPadding
|
||||||
import com.vitorpamplona.amethyst.model.nip11RelayInfo.Nip11CachedRetriever
|
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -68,6 +68,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|||||||
import com.vitorpamplona.amethyst.Amethyst
|
import com.vitorpamplona.amethyst.Amethyst
|
||||||
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
||||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||||
|
import com.vitorpamplona.amethyst.commons.relays.nip11.Nip11CachedRetriever
|
||||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||||
import com.vitorpamplona.amethyst.commons.resources.cancel
|
import com.vitorpamplona.amethyst.commons.resources.cancel
|
||||||
import com.vitorpamplona.amethyst.commons.resources.confirm
|
import com.vitorpamplona.amethyst.commons.resources.confirm
|
||||||
@@ -96,7 +97,6 @@ import com.vitorpamplona.amethyst.commons.ui.stringRes
|
|||||||
import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness
|
import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness
|
||||||
import com.vitorpamplona.amethyst.commons.ui.theme.HorzHalfVertPadding
|
import com.vitorpamplona.amethyst.commons.ui.theme.HorzHalfVertPadding
|
||||||
import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn
|
import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn
|
||||||
import com.vitorpamplona.amethyst.model.nip11RelayInfo.Nip11CachedRetriever
|
|
||||||
import com.vitorpamplona.amethyst.ui.note.formatMediumDateTime
|
import com.vitorpamplona.amethyst.ui.note.formatMediumDateTime
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
|
||||||
|
|||||||
+1
-1
@@ -26,7 +26,7 @@ import com.vitorpamplona.quartz.utils.concurrent.ConcurrentMap
|
|||||||
/**
|
/**
|
||||||
* In-memory fallback implementation of [MlsGroupStateStore].
|
* In-memory fallback implementation of [MlsGroupStateStore].
|
||||||
*
|
*
|
||||||
* Used only when [AndroidMlsGroupStateStore] cannot be initialized (e.g., when the
|
* Used only when [EncryptedMlsGroupStateStore] cannot be initialized (e.g., when the
|
||||||
* Android KeyStore is unavailable). State is lost on app restart, but this lets
|
* Android KeyStore is unavailable). State is lost on app restart, but this lets
|
||||||
* Marmot group operations at least work within a single session instead of failing
|
* Marmot group operations at least work within a single session instead of failing
|
||||||
* with "Marmot not initialized".
|
* with "Marmot not initialized".
|
||||||
|
|||||||
+1
-1
@@ -58,7 +58,7 @@ import java.io.RandomAccessFile
|
|||||||
*
|
*
|
||||||
* **Not thread-safe.** Entries are cached in memory so an append never has to
|
* **Not thread-safe.** Entries are cached in memory so an append never has to
|
||||||
* read the log back, and that cache assumes one owner. Callers hold their own
|
* read the log back, and that cache assumes one owner. Callers hold their own
|
||||||
* lock around every method (see `AndroidMarmotMessageStore`), and one instance
|
* lock around every method (see `EncryptedMarmotMessageStore`), and one instance
|
||||||
* must own any given file.
|
* must own any given file.
|
||||||
*
|
*
|
||||||
* @param encrypt must produce a self-describing blob — it carries its own IV /
|
* @param encrypt must produce a self-describing blob — it carries its own IV /
|
||||||
|
|||||||
+7
-7
@@ -18,7 +18,7 @@
|
|||||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.model.marmot
|
package com.vitorpamplona.amethyst.commons.marmot
|
||||||
|
|
||||||
import com.vitorpamplona.amethyst.commons.model.preferences.SecretEncryption
|
import com.vitorpamplona.amethyst.commons.model.preferences.SecretEncryption
|
||||||
import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageBundleStore
|
import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageBundleStore
|
||||||
@@ -30,7 +30,7 @@ import kotlinx.coroutines.withContext
|
|||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Android implementation of [KeyPackageBundleStore] using file-based encrypted storage.
|
* File-backed [KeyPackageBundleStore], encrypted at rest.
|
||||||
*
|
*
|
||||||
* Storage layout:
|
* Storage layout:
|
||||||
* ```
|
* ```
|
||||||
@@ -40,10 +40,10 @@ import java.io.File
|
|||||||
* The blob contains private key material — init keys, encryption keys,
|
* The blob contains private key material — init keys, encryption keys,
|
||||||
* signature keys — that the MLS engine needs to process Welcome events
|
* signature keys — that the MLS engine needs to process Welcome events
|
||||||
* received days or weeks after the corresponding KeyPackage was published.
|
* received days or weeks after the corresponding KeyPackage was published.
|
||||||
* It is encrypted at rest with [SecretEncryption] (AES/GCM via Android
|
* It is encrypted at rest with [SecretEncryption] (AES-256-GCM, keyed by the platform's
|
||||||
* KeyStore), the same primitive used by [AndroidMlsGroupStateStore].
|
* keystore), the same primitive used by [EncryptedMlsGroupStateStore].
|
||||||
*/
|
*/
|
||||||
class AndroidKeyPackageBundleStore(
|
class EncryptedKeyPackageBundleStore(
|
||||||
private val rootDir: File,
|
private val rootDir: File,
|
||||||
private val encryption: SecretEncryption = SecretEncryption(),
|
private val encryption: SecretEncryption = SecretEncryption(),
|
||||||
) : KeyPackageBundleStore {
|
) : KeyPackageBundleStore {
|
||||||
@@ -51,7 +51,7 @@ class AndroidKeyPackageBundleStore(
|
|||||||
|
|
||||||
init {
|
init {
|
||||||
Log.d(TAG) {
|
Log.d(TAG) {
|
||||||
"Initialized AndroidKeyPackageBundleStore: rootDir=${rootDir.absolutePath}"
|
"Initialized EncryptedKeyPackageBundleStore: rootDir=${rootDir.absolutePath}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,6 +121,6 @@ class AndroidKeyPackageBundleStore(
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "AndroidKeyPackageBundleStore"
|
private const val TAG = "EncryptedKeyPackageBundleStore"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+6
-6
@@ -18,7 +18,7 @@
|
|||||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.model.marmot
|
package com.vitorpamplona.amethyst.commons.marmot
|
||||||
|
|
||||||
import com.vitorpamplona.amethyst.commons.marmot.EncryptedAppendLog
|
import com.vitorpamplona.amethyst.commons.marmot.EncryptedAppendLog
|
||||||
import com.vitorpamplona.amethyst.commons.model.preferences.SecretEncryption
|
import com.vitorpamplona.amethyst.commons.model.preferences.SecretEncryption
|
||||||
@@ -32,9 +32,9 @@ import kotlinx.coroutines.withContext
|
|||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Android implementation of [MarmotMessageStore] using file-based encrypted storage.
|
* File-backed [MarmotMessageStore], encrypted at rest.
|
||||||
*
|
*
|
||||||
* Stored alongside the [AndroidMlsGroupStateStore] data:
|
* Stored alongside the [EncryptedMlsGroupStateStore] data:
|
||||||
* ```
|
* ```
|
||||||
* <rootDir>/mls_groups/<nostrGroupId>/messages — encrypted message log
|
* <rootDir>/mls_groups/<nostrGroupId>/messages — encrypted message log
|
||||||
* ```
|
* ```
|
||||||
@@ -44,7 +44,7 @@ import java.io.File
|
|||||||
* small encrypted segment instead of rewriting the conversation, which is what
|
* small encrypted segment instead of rewriting the conversation, which is what
|
||||||
* keeps the cost of a send flat as the history grows.
|
* keeps the cost of a send flat as the history grows.
|
||||||
*/
|
*/
|
||||||
class AndroidMarmotMessageStore(
|
class EncryptedMarmotMessageStore(
|
||||||
private val rootDir: File,
|
private val rootDir: File,
|
||||||
private val encryption: SecretEncryption = SecretEncryption(),
|
private val encryption: SecretEncryption = SecretEncryption(),
|
||||||
) : MarmotMessageStore {
|
) : MarmotMessageStore {
|
||||||
@@ -52,7 +52,7 @@ class AndroidMarmotMessageStore(
|
|||||||
|
|
||||||
init {
|
init {
|
||||||
Log.d(TAG) {
|
Log.d(TAG) {
|
||||||
"Initialized AndroidMarmotMessageStore: rootDir=${rootDir.absolutePath}"
|
"Initialized EncryptedMarmotMessageStore: rootDir=${rootDir.absolutePath}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -353,7 +353,7 @@ class AndroidMarmotMessageStore(
|
|||||||
) = log.rewrite(file, messages)
|
) = log.rewrite(file, messages)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "AndroidMarmotMessageStore"
|
private const val TAG = "EncryptedMarmotMessageStore"
|
||||||
private val HEX_PATTERN = Regex("^[0-9a-fA-F]+$")
|
private val HEX_PATTERN = Regex("^[0-9a-fA-F]+$")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+7
-7
@@ -18,7 +18,7 @@
|
|||||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.model.marmot
|
package com.vitorpamplona.amethyst.commons.marmot
|
||||||
|
|
||||||
import com.vitorpamplona.amethyst.commons.model.preferences.SecretEncryption
|
import com.vitorpamplona.amethyst.commons.model.preferences.SecretEncryption
|
||||||
import com.vitorpamplona.quartz.marmot.mls.group.MlsGroupStateStore
|
import com.vitorpamplona.quartz.marmot.mls.group.MlsGroupStateStore
|
||||||
@@ -29,10 +29,10 @@ import java.io.File
|
|||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Android implementation of [MlsGroupStateStore] using file-based encrypted storage.
|
* File-backed [MlsGroupStateStore], encrypted at rest.
|
||||||
*
|
*
|
||||||
* All MLS group state (containing private keys and epoch secrets) is encrypted
|
* All MLS group state (containing private keys and epoch secrets) is encrypted
|
||||||
* at rest using [SecretEncryption] (AES/GCM backed by Android KeyStore).
|
* at rest using [SecretEncryption] (AES-256-GCM, keyed by the platform's keystore).
|
||||||
*
|
*
|
||||||
* Storage layout:
|
* Storage layout:
|
||||||
* ```
|
* ```
|
||||||
@@ -41,13 +41,13 @@ import java.io.FileOutputStream
|
|||||||
* <rootDir>/mls_groups/<nostrGroupId>/ratchet — encrypted OwnSenderRatchet
|
* <rootDir>/mls_groups/<nostrGroupId>/ratchet — encrypted OwnSenderRatchet
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
class AndroidMlsGroupStateStore(
|
class EncryptedMlsGroupStateStore(
|
||||||
private val rootDir: File,
|
private val rootDir: File,
|
||||||
private val encryption: SecretEncryption = SecretEncryption(),
|
private val encryption: SecretEncryption = SecretEncryption(),
|
||||||
) : MlsGroupStateStore {
|
) : MlsGroupStateStore {
|
||||||
init {
|
init {
|
||||||
Log.d(TAG) {
|
Log.d(TAG) {
|
||||||
"Initialized AndroidMlsGroupStateStore: rootDir=${rootDir.absolutePath}, " +
|
"Initialized EncryptedMlsGroupStateStore: rootDir=${rootDir.absolutePath}, " +
|
||||||
"mls_groups exists=${File(rootDir, "mls_groups").exists()}"
|
"mls_groups exists=${File(rootDir, "mls_groups").exists()}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -61,7 +61,7 @@ class AndroidMlsGroupStateStore(
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "AndroidMlsGroupStateStore"
|
private const val TAG = "EncryptedMlsGroupStateStore"
|
||||||
private val HEX_PATTERN = Regex("^[0-9a-fA-F]+$")
|
private val HEX_PATTERN = Regex("^[0-9a-fA-F]+$")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,7 +274,7 @@ class AndroidMlsGroupStateStore(
|
|||||||
// Fallback: if rename fails (e.g., cross-filesystem), copy and delete
|
// Fallback: if rename fails (e.g., cross-filesystem), copy and delete
|
||||||
tempFile.copyTo(target, overwrite = true)
|
tempFile.copyTo(target, overwrite = true)
|
||||||
if (!tempFile.delete()) {
|
if (!tempFile.delete()) {
|
||||||
Log.w("AndroidMlsGroupStateStore") { "Failed to delete temp file after copy fallback: ${tempFile.absolutePath}" }
|
Log.w("EncryptedMlsGroupStateStore") { "Failed to delete temp file after copy fallback: ${tempFile.absolutePath}" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+4
-4
@@ -18,7 +18,7 @@
|
|||||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.model.marmot
|
package com.vitorpamplona.amethyst.commons.marmot
|
||||||
|
|
||||||
import com.vitorpamplona.amethyst.commons.model.preferences.SecretEncryption
|
import com.vitorpamplona.amethyst.commons.model.preferences.SecretEncryption
|
||||||
import com.vitorpamplona.quartz.marmot.protocolCore.MarmotPublishObligationStore
|
import com.vitorpamplona.quartz.marmot.protocolCore.MarmotPublishObligationStore
|
||||||
@@ -31,7 +31,7 @@ import kotlinx.coroutines.withContext
|
|||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Android implementation of [MarmotPublishObligationStore], encrypted at rest
|
* File-backed [MarmotPublishObligationStore], encrypted at rest
|
||||||
* with [SecretEncryption] like the group-state and KeyPackage stores.
|
* with [SecretEncryption] like the group-state and KeyPackage stores.
|
||||||
*
|
*
|
||||||
* ```
|
* ```
|
||||||
@@ -49,7 +49,7 @@ import java.io.File
|
|||||||
* concurrently and resolve out of order, so removing one record must not
|
* concurrently and resolve out of order, so removing one record must not
|
||||||
* rewrite another's.
|
* rewrite another's.
|
||||||
*/
|
*/
|
||||||
class AndroidPublishObligationStore(
|
class EncryptedPublishObligationStore(
|
||||||
private val rootDir: File,
|
private val rootDir: File,
|
||||||
private val encryption: SecretEncryption = SecretEncryption(),
|
private val encryption: SecretEncryption = SecretEncryption(),
|
||||||
) : MarmotPublishObligationStore {
|
) : MarmotPublishObligationStore {
|
||||||
@@ -184,6 +184,6 @@ class AndroidPublishObligationStore(
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "AndroidPublishObligationStore"
|
private const val TAG = "EncryptedPublishObligationStore"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+8
-3
@@ -18,9 +18,9 @@
|
|||||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.model.nip11RelayInfo
|
package com.vitorpamplona.amethyst.commons.relays.nip11
|
||||||
|
|
||||||
import android.util.LruCache
|
import androidx.collection.LruCache
|
||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl
|
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl
|
||||||
@@ -33,7 +33,12 @@ class Nip11CachedRetriever(
|
|||||||
val okHttpClient: (NormalizedRelayUrl) -> OkHttpClient,
|
val okHttpClient: (NormalizedRelayUrl) -> OkHttpClient,
|
||||||
) {
|
) {
|
||||||
private val relayInformationEmptyCache = LruCache<NormalizedRelayUrl, Nip11RelayInformation>(1000)
|
private val relayInformationEmptyCache = LruCache<NormalizedRelayUrl, Nip11RelayInformation>(1000)
|
||||||
private val relayInformationDocumentCache = LruCache<NormalizedRelayUrl, RetrieveResult?>(1000)
|
|
||||||
|
// Value type is non-null: androidx.collection.LruCache bounds V to Any, and every put here
|
||||||
|
// stores a concrete RetrieveResult. The old android.util.LruCache was a Java platform type, so
|
||||||
|
// the nullable argument compiled but never meant anything — get() returns null on a miss either
|
||||||
|
// way, which is what the readers below already branch on.
|
||||||
|
private val relayInformationDocumentCache = LruCache<NormalizedRelayUrl, RetrieveResult>(1000)
|
||||||
private val retriever = Nip11Retriever(okHttpClient)
|
private val retriever = Nip11Retriever(okHttpClient)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
+1
-1
@@ -18,7 +18,7 @@
|
|||||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.model.nip11RelayInfo
|
package com.vitorpamplona.amethyst.commons.relays.nip11
|
||||||
|
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.toHttp
|
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.toHttp
|
||||||
+1
-1
@@ -18,7 +18,7 @@
|
|||||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.model.nip11RelayInfo
|
package com.vitorpamplona.amethyst.commons.relays.nip11
|
||||||
|
|
||||||
import com.vitorpamplona.quartz.nip11RelayInfo.Nip11RelayInformation
|
import com.vitorpamplona.quartz.nip11RelayInfo.Nip11RelayInformation
|
||||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||||
+2
-2
@@ -18,9 +18,9 @@
|
|||||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.service
|
package com.vitorpamplona.amethyst.commons.service
|
||||||
|
|
||||||
import android.util.LruCache
|
import androidx.collection.LruCache
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import com.vitorpamplona.quartz.utils.Log
|
import com.vitorpamplona.quartz.utils.Log
|
||||||
import com.vitorpamplona.quartz.utils.RandomInstance
|
import com.vitorpamplona.quartz.utils.RandomInstance
|
||||||
+184
@@ -0,0 +1,184 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.commons.marmot
|
||||||
|
|
||||||
|
import com.vitorpamplona.amethyst.commons.model.preferences.SecretEncryption
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertNull
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.rules.TemporaryFolder
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Round trips for the file-backed Marmot stores, which had no coverage while they sat in `amethyst/`
|
||||||
|
* behind an `Android` prefix they never earned.
|
||||||
|
*
|
||||||
|
* What matters here is that the bytes survive a restart: these hold MLS group state and the message
|
||||||
|
* log, so a store that writes but cannot read back loses a group's history and its ratchet — and MLS
|
||||||
|
* state that cannot be reloaded is not recoverable from the relays.
|
||||||
|
*/
|
||||||
|
class EncryptedMarmotStoresTest {
|
||||||
|
@get:Rule
|
||||||
|
val folder = TemporaryFolder()
|
||||||
|
|
||||||
|
private var seq = 0
|
||||||
|
|
||||||
|
/** A fresh account directory plus its own key file, so tests cannot read each other's data. */
|
||||||
|
private fun accountDir(): Pair<File, SecretEncryption> {
|
||||||
|
val n = seq++
|
||||||
|
val dir = folder.newFolder("account_$n")
|
||||||
|
return dir to SecretEncryption(File(folder.root, "secret_$n.key"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group ids must be hex: both stores validate, which is what stops a crafted id escaping the
|
||||||
|
// account directory. Using a realistic 64-char id rather than a label keeps the tests honest.
|
||||||
|
private val groupId = "a".repeat(63) + "1"
|
||||||
|
private val otherGroupId = "b".repeat(63) + "2"
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun groupStateSurvivesANewStoreOverTheSameDirectory() =
|
||||||
|
runTest {
|
||||||
|
val (dir, encryption) = accountDir()
|
||||||
|
val payload = byteArrayOf(1, 2, 3, 4, 5)
|
||||||
|
|
||||||
|
EncryptedMlsGroupStateStore(dir, encryption).save(groupId, payload)
|
||||||
|
|
||||||
|
val reopened = EncryptedMlsGroupStateStore(dir, encryption).load(groupId)
|
||||||
|
assertEquals("the same bytes come back", payload.toList(), reopened?.toList())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun anUnknownGroupLoadsAsNull() =
|
||||||
|
runTest {
|
||||||
|
val (dir, encryption) = accountDir()
|
||||||
|
|
||||||
|
assertNull(EncryptedMlsGroupStateStore(dir, encryption).load("c".repeat(63) + "3"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun deletingAGroupRemovesItFromTheListing() =
|
||||||
|
runTest {
|
||||||
|
val (dir, encryption) = accountDir()
|
||||||
|
val store = EncryptedMlsGroupStateStore(dir, encryption)
|
||||||
|
store.save(groupId, byteArrayOf(9))
|
||||||
|
store.save(otherGroupId, byteArrayOf(8))
|
||||||
|
|
||||||
|
store.delete(groupId)
|
||||||
|
|
||||||
|
assertEquals("only the other group is left", listOf(otherGroupId), store.listGroups())
|
||||||
|
assertNull("and its state is gone", store.load(groupId))
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The sender ratchet is stored separately from the group blob; losing it breaks decryption. */
|
||||||
|
@Test
|
||||||
|
fun theSenderRatchetRoundTripsIndependentlyOfTheGroupState() =
|
||||||
|
runTest {
|
||||||
|
val (dir, encryption) = accountDir()
|
||||||
|
val store = EncryptedMlsGroupStateStore(dir, encryption)
|
||||||
|
|
||||||
|
store.save(groupId, byteArrayOf(1))
|
||||||
|
store.saveSenderRatchet(groupId, byteArrayOf(7, 7, 7))
|
||||||
|
|
||||||
|
val reopened = EncryptedMlsGroupStateStore(dir, encryption)
|
||||||
|
assertEquals("ratchet preserved", listOf<Byte>(7, 7, 7), reopened.loadSenderRatchet(groupId)?.toList())
|
||||||
|
assertEquals("and the group blob is untouched", listOf<Byte>(1), reopened.load(groupId)?.toList())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun appendedMessagesComeBackInOrderAfterAReopen() =
|
||||||
|
runTest {
|
||||||
|
val (dir, encryption) = accountDir()
|
||||||
|
val store = EncryptedMarmotMessageStore(dir, encryption)
|
||||||
|
|
||||||
|
store.appendMessage(groupId, """{"id":"one"}""")
|
||||||
|
store.appendMessage(groupId, """{"id":"two"}""")
|
||||||
|
|
||||||
|
val reopened = EncryptedMarmotMessageStore(dir, encryption).loadMessages(groupId)
|
||||||
|
assertEquals("both, in append order", listOf("""{"id":"one"}""", """{"id":"two"}"""), reopened)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun aGroupWithNoMessagesLoadsEmptyRatherThanFailing() =
|
||||||
|
runTest {
|
||||||
|
val (dir, encryption) = accountDir()
|
||||||
|
|
||||||
|
assertTrue(EncryptedMarmotMessageStore(dir, encryption).loadMessages("d".repeat(63) + "4").isEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun deletingAGroupDropsItsMessageLog() =
|
||||||
|
runTest {
|
||||||
|
val (dir, encryption) = accountDir()
|
||||||
|
val store = EncryptedMarmotMessageStore(dir, encryption)
|
||||||
|
store.appendMessage(groupId, """{"id":"one"}""")
|
||||||
|
|
||||||
|
store.delete(groupId)
|
||||||
|
|
||||||
|
assertTrue(EncryptedMarmotMessageStore(dir, encryption).loadMessages(groupId).isEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The group snapshot is what a cold start restores from before replaying the log. */
|
||||||
|
@Test
|
||||||
|
fun theGroupSnapshotRoundTrips() =
|
||||||
|
runTest {
|
||||||
|
val (dir, encryption) = accountDir()
|
||||||
|
val store = EncryptedMarmotMessageStore(dir, encryption)
|
||||||
|
|
||||||
|
store.recordGroupSnapshot(groupId, """{"epoch":4}""")
|
||||||
|
|
||||||
|
assertEquals("""{"epoch":4}""", EncryptedMarmotMessageStore(dir, encryption).loadGroupSnapshot(groupId))
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Two accounts are two directories: one must never read the other's groups. */
|
||||||
|
@Test
|
||||||
|
fun twoAccountDirectoriesDoNotSeeEachOther() =
|
||||||
|
runTest {
|
||||||
|
val (dirA, encA) = accountDir()
|
||||||
|
val (dirB, encB) = accountDir()
|
||||||
|
|
||||||
|
EncryptedMlsGroupStateStore(dirA, encA).save(groupId, byteArrayOf(1))
|
||||||
|
|
||||||
|
assertNull("B cannot see A's group", EncryptedMlsGroupStateStore(dirB, encB).load(groupId))
|
||||||
|
assertTrue("nor list it", EncryptedMlsGroupStateStore(dirB, encB).listGroups().isEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The hex check is a path-traversal guard: a crafted id must not be able to leave the account dir. */
|
||||||
|
@Test
|
||||||
|
fun aNonHexGroupIdIsRejected() =
|
||||||
|
runTest {
|
||||||
|
val (dir, encryption) = accountDir()
|
||||||
|
val store = EncryptedMlsGroupStateStore(dir, encryption)
|
||||||
|
|
||||||
|
listOf("../escape", "not hex", "abc/def", "").forEach { bad ->
|
||||||
|
val thrown =
|
||||||
|
try {
|
||||||
|
store.load(bad)
|
||||||
|
false
|
||||||
|
} catch (e: IllegalArgumentException) {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
assertTrue("\"$bad\" must be rejected, not resolved to a path", thrown)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -18,7 +18,7 @@
|
|||||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.model.nip11RelayInfo
|
package com.vitorpamplona.amethyst.commons.relays.nip11
|
||||||
|
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
+117
@@ -0,0 +1,117 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.commons.service
|
||||||
|
|
||||||
|
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||||
|
import org.junit.Assert.assertFalse
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [OnlineChecker]'s cache predicates — the part that decides, without touching the network, whether a
|
||||||
|
* media URL is known-good, known-bad, or unknown.
|
||||||
|
*
|
||||||
|
* These decide whether the UI shows a player or a "this is offline" placeholder, and a stale entry
|
||||||
|
* being trusted is the difference between a video that plays and one that never gets retried. Nothing
|
||||||
|
* here had coverage while the object sat in `amethyst/`.
|
||||||
|
*
|
||||||
|
* The suspend `isOnline` probe is deliberately not exercised: it needs a real OkHttp round trip and
|
||||||
|
* `commons` has no MockWebServer, so there is no honest way to drive it from here.
|
||||||
|
*/
|
||||||
|
class OnlineCheckerTest {
|
||||||
|
private val url = "https://example.com/video.mp4"
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun clearSharedCache() {
|
||||||
|
// OnlineChecker is an object, so its LruCache outlives each test.
|
||||||
|
OnlineChecker.checkOnlineCache.evictAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun seed(
|
||||||
|
online: Boolean,
|
||||||
|
ageSeconds: Long,
|
||||||
|
) {
|
||||||
|
OnlineChecker.checkOnlineCache.put(url, OnlineCheckResult(TimeUtils.now() - ageSeconds, online))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun anUnknownUrlIsNeitherOnlineNorKnownOffline() {
|
||||||
|
assertFalse("nothing cached, so not known online", OnlineChecker.isOnlineCached(url))
|
||||||
|
assertFalse("and not known offline either", OnlineChecker.isCachedAndOffline(url))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun aFreshOnlineEntryReadsOnline() {
|
||||||
|
seed(online = true, ageSeconds = 10)
|
||||||
|
|
||||||
|
assertTrue(OnlineChecker.isOnlineCached(url))
|
||||||
|
assertFalse("an online entry is not 'cached and offline'", OnlineChecker.isCachedAndOffline(url))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun aFreshOfflineEntryReadsOffline() {
|
||||||
|
seed(online = false, ageSeconds = 10)
|
||||||
|
|
||||||
|
assertTrue(OnlineChecker.isCachedAndOffline(url))
|
||||||
|
assertFalse("and must not read as online", OnlineChecker.isOnlineCached(url))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The five-minute TTL in both directions. Trusting a stale *online* entry shows a player for
|
||||||
|
* something that has since gone; trusting a stale *offline* one never retries a URL that came back.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
fun anEntryOlderThanFiveMinutesIsTrustedForNothing() {
|
||||||
|
seed(online = true, ageSeconds = 301)
|
||||||
|
assertFalse("a stale online entry is no longer online", OnlineChecker.isOnlineCached(url))
|
||||||
|
|
||||||
|
seed(online = false, ageSeconds = 301)
|
||||||
|
assertFalse("and a stale offline entry no longer counts as known-offline", OnlineChecker.isCachedAndOffline(url))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun anEntryJustInsideFiveMinutesIsStillTrusted() {
|
||||||
|
seed(online = true, ageSeconds = 290)
|
||||||
|
|
||||||
|
assertTrue(OnlineChecker.isOnlineCached(url))
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Retry is for failures only: dropping a good entry would refetch every URL that already worked. */
|
||||||
|
@Test
|
||||||
|
fun resetIfOfflineToRetryDropsOnlyTheOfflineEntries() {
|
||||||
|
seed(online = false, ageSeconds = 10)
|
||||||
|
OnlineChecker.resetIfOfflineToRetry(url)
|
||||||
|
assertFalse("the offline entry is gone, so the next check refetches", OnlineChecker.isCachedAndOffline(url))
|
||||||
|
|
||||||
|
seed(online = true, ageSeconds = 10)
|
||||||
|
OnlineChecker.resetIfOfflineToRetry(url)
|
||||||
|
assertTrue("the online entry survived", OnlineChecker.isOnlineCached(url))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun aBlankOrNullUrlIsNeverOnline() {
|
||||||
|
assertFalse(OnlineChecker.isOnlineCached(null))
|
||||||
|
assertFalse(OnlineChecker.isOnlineCached(" "))
|
||||||
|
assertFalse(OnlineChecker.isCachedAndOffline(null))
|
||||||
|
assertFalse(OnlineChecker.isCachedAndOffline(" "))
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user