diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 309f52fb2..61e76d85f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -61,13 +61,13 @@ jobs: uses: actions/upload-artifact@v4 with: name: Play Benchmark APK - path: amethyst/build/outputs/apk/play/debug/amethyst-play-universal-benchmark.apk + path: amethyst/build/outputs/apk/play/benchmark/amethyst-play-universal-benchmark.apk - name: Upload FDroid APK Benchmark uses: actions/upload-artifact@v4 with: name: FDroid Benchmark APK - path: amethyst/build/outputs/apk/fdroid/debug/amethyst-fdroid-universal-benchmark.apk + path: amethyst/build/outputs/apk/fdroid/benchmark/amethyst-fdroid-universal-benchmark.apk - name: Upload Compose Reports uses: actions/upload-artifact@v4 @@ -75,9 +75,6 @@ jobs: name: Compose Reports path: amethyst/build/compose_compiler - - name: Tests - run: ./gradlew test --no-daemon - - name: Upload Test Results uses: actions/upload-artifact@v4 with: diff --git a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ThreadDualAxisChartAssemblerTest.kt b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ThreadDualAxisChartAssemblerTest.kt index 99db5b628..1292e6b5f 100644 --- a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ThreadDualAxisChartAssemblerTest.kt +++ b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ThreadDualAxisChartAssemblerTest.kt @@ -27,13 +27,17 @@ import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.location.LocationState import com.vitorpamplona.amethyst.service.okhttp.OkHttpWebSocket +import com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc.NWCPaymentFilterAssembler import com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.dal.ThreadFeedFilter import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper +import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip01Core.verify +import com.vitorpamplona.quartz.nip03Timestamp.ots.okhttp.OkHttpOtsResolverBuilder +import com.vitorpamplona.quartz.nip03Timestamp.ots.okhttp.OtsBlockHeightCache import junit.framework.TestCase import junit.framework.TestCase.assertEquals import kotlinx.coroutines.CoroutineScope @@ -67,6 +71,25 @@ class ThreadDualAxisChartAssemblerTest { }, CoroutineScope(Dispatchers.IO + SupervisorJob()), ), + nwcFilterAssembler = + NWCPaymentFilterAssembler( + NostrClient( + OkHttpWebSocket.Builder { + OkHttpClient + .Builder() + .followRedirects(true) + .followSslRedirects(true) + .build() + }, + CoroutineScope(Dispatchers.IO + SupervisorJob()), + ), + ), + otsResolverBuilder = + OkHttpOtsResolverBuilder( + okHttpClient = { OkHttpClient.Builder().build() }, + isTorActive = { false }, + cache = OtsBlockHeightCache(), + ), ) val db = diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/TorSharedPreferences.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/TorSharedPreferences.kt index af7fdaa96..89a19d8c3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/TorSharedPreferences.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/TorSharedPreferences.kt @@ -27,7 +27,6 @@ import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.intPreferencesKey import androidx.datastore.preferences.core.stringPreferencesKey -import com.vitorpamplona.amethyst.ui.actions.MediaSaverToDisk.save import com.vitorpamplona.amethyst.ui.tor.TorSettings import com.vitorpamplona.amethyst.ui.tor.TorSettingsFlow import com.vitorpamplona.amethyst.ui.tor.TorType diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/AccountsTorStateConnector.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/AccountsTorStateConnector.kt index cea459d20..538ac3c50 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/AccountsTorStateConnector.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/AccountsTorStateConnector.kt @@ -34,7 +34,6 @@ import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.transformLatest -import kotlin.collections.ifEmpty class AccountsTorStateConnector( accountsCache: AccountCacheState, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/lnurl/LightningAddressResolver.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/lnurl/LightningAddressResolver.kt index 42cc045fa..c814e07ea 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/lnurl/LightningAddressResolver.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/lnurl/LightningAddressResolver.kt @@ -172,7 +172,7 @@ class LightningAddressResolver { val messageNode = tree.get("message") val statusNode = tree.get("status") - if (tree.get("error").isBoolean && messageNode != null) { + if (tree.get("error") != null && tree.get("error").isBoolean && messageNode != null) { if (errorNode.asBoolean()) { return messageNode.asText() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/EventNotificationConsumer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/EventNotificationConsumer.kt index 3b0727d02..f5e625416 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/EventNotificationConsumer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/EventNotificationConsumer.kt @@ -29,7 +29,6 @@ import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.LocalCache.chatroomList import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.sendDMNotification import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.sendZapNotification diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpClientFactory.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpClientFactory.kt index 9489f8a06..ec055e448 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpClientFactory.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpClientFactory.kt @@ -20,6 +20,8 @@ */ package com.vitorpamplona.amethyst.service.okhttp +import android.os.Build +import android.util.Log import okhttp3.Dispatcher import okhttp3.OkHttpClient import java.net.InetSocketAddress @@ -35,6 +37,22 @@ class OkHttpClientFactory( const val DEFAULT_IS_MOBILE: Boolean = false const val DEFAULT_TIMEOUT_ON_WIFI_SECS: Int = 10 const val DEFAULT_TIMEOUT_ON_MOBILE_SECS: Int = 30 + + private fun isEmulator(): Boolean = + Build.FINGERPRINT.startsWith("generic") || + Build.FINGERPRINT.lowercase().contains("emulator") || + Build.MODEL.contains("google_sdk") || + Build.MODEL.lowercase().contains("droid4x") || + Build.MODEL.contains("Emulator") || + Build.MODEL.contains("Android SDK built for x86") || + Build.MANUFACTURER.contains("Genymotion") || + (Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic")) || + "google_sdk" == Build.PRODUCT || + Build.HARDWARE.contains("goldfish") || + Build.HARDWARE.contains("ranchu") || + Build.HARDWARE.contains("vbox86") || + Build.HARDWARE.contains("nox") || + Build.HARDWARE.contains("cuttlefish") } val logging = LoggingInterceptor() @@ -42,22 +60,12 @@ class OkHttpClientFactory( val myDispatcher = Dispatcher().apply { - maxRequests = 512 - } - - /* - DEBUG OK HTTP connections here. - init { - if (isDebug) { - GlobalScope.launch(Dispatchers.IO) { - while (true) { - Log.d("OkHttpClientFactory", "Active threads ${myDispatcher.runningCallsCount()}") - delay(5000) - } + if (!isEmulator()) { + maxRequests = 512 + } else { + Log.i("OkHttpClientFactory", "Emulator detected, using default maxRequests: 64.") } } - } - */ private val rootClient = OkHttpClient diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelObservers.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelObservers.kt index 4400ed68a..0e60c26c7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelObservers.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelObservers.kt @@ -26,7 +26,6 @@ import androidx.compose.runtime.remember import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.model.Channel import com.vitorpamplona.amethyst.model.ChannelState -import com.vitorpamplona.amethyst.model.LocalCache.notes import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt index 2b8a9e1f4..9cb4de2e4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt @@ -28,7 +28,6 @@ import androidx.activity.enableEdgeToEdge import androidx.annotation.RequiresApi import androidx.appcompat.app.AppCompatActivity import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.debugState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/privacy/PrivacyOptionsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/privacy/PrivacyOptionsScreen.kt index 2ea3f5fc3..887a7b177 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/privacy/PrivacyOptionsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/privacy/PrivacyOptionsScreen.kt @@ -33,7 +33,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.preferences.AccountPreferenceStores.Companion.torSettings import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.topbars.SavingTopBar import com.vitorpamplona.amethyst.ui.tor.PrivacySettingsBody diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelayFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelayFeedViewModel.kt index 2ebbcd3fe..078ed2623 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelayFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelayFeedViewModel.kt @@ -41,12 +41,10 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.transformLatest import kotlinx.coroutines.launch -import kotlin.collections.map @Stable class RelayFeedViewModel : diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelaysTabHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelaysTabHeader.kt index 58ade6e62..4c211d588 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelaysTabHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelaysTabHeader.kt @@ -22,7 +22,6 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.relays import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoDialog.kt index 6e036b25b..575c6a1d7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoDialog.kt @@ -21,7 +21,6 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/TorSettingsSetup.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/TorSettingsSetup.kt index c991b54df..d23445c1f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/TorSettingsSetup.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/TorSettingsSetup.kt @@ -32,7 +32,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.preferences.AccountPreferenceStores.Companion.torSettings import com.vitorpamplona.amethyst.ui.components.appendLink import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.tor.ConnectTorDialog diff --git a/amethyst/src/main/res/values-sl-rSI/strings.xml b/amethyst/src/main/res/values-sl-rSI/strings.xml index 115c30c60..6435c791f 100644 --- a/amethyst/src/main/res/values-sl-rSI/strings.xml +++ b/amethyst/src/main/res/values-sl-rSI/strings.xml @@ -59,6 +59,16 @@ Prijavi se s privatnim ključem, za odstranitev iz seznama sledljivih Prijavi se s privatnim ključem, za cenzuro besed ali stavkov Uporabljaš javni ključ in javni ključi omogočajo le branje. Prijavi se s privatnim ključem, za prikaz skritih besed in stavkov + Uporabljaš javni ključ in javni ključi omogočajo le branje. +Za spreminjanje nastavitev, se prijavi s privatnim ključem + Uporabljaš javni ključ in javni ključi omogočajo le branje. +Za nalaganje se je potrebno prijaviti s privatnim ključem + Uporabljaš javni ključ in javni ključi omogočajo le branje. +Za podpisovanje se je potrebno prijaviti s privatnim ključem + Dešifriranje nima pooblastila + Podpisnik ni odobril dešifriranja, ki je potrebno za to operacijo. V aplikaciji za podpisovanje aktivirajte NIP-44 možnost dešifriranja in poskusite znova + Ne najdem podpisnika + Ali je bila aplikacija za podpisovanje odstranjena? Preverite, ali je aplikacija za podpisovanje nameščena in ima dostop do tega računa. Odjavite se in ponovno prijavite, če se je aplikacija morda spremenila. Zapi Števec vpogledov Posreduj @@ -106,6 +116,7 @@ Prijavi se s privatnim ključem, za prikaz skritih besed in stavkov Moja vrhunska skupina Url slike Opis + Ne najdem opisa "O nas.. " Kaj imaš v mislih? Napiši sporočilo… @@ -147,6 +158,11 @@ Prijavi se s privatnim ključem, za prikaz skritih besed in stavkov Video shranjen v video galerijo telefona Neuspešno shranjevanje videa Naloži sliko + Zajemi sliko + Posnemi video + Posnemi sporočilo + Posnemi sporočilo + Klikni in drži za snemanje sporočila Nalaganje … Uporabnik nima nastavljenega \"lightning\" naslova za sprejem satoshi-jev "odgovori tukaj.. " @@ -159,6 +175,8 @@ Prijavi se s privatnim ključem, za prikaz skritih besed in stavkov Blokirani uporabniki Nove objave Pogovori + Vir + Moderatorska vrsta Zapiski Pogovori Tvoje @@ -239,6 +257,8 @@ Prijavi se s privatnim ključem, za prikaz skritih besed in stavkov Releji Vnesi 1–3 releje, ki gostijo to skupino. Nostr odjemalci uporabljajo to nastavitev, da vedo, od kod prenesti tvoja sporočila in kam jih poslati. + Plačljiv rele + Vsili Tor pri povezovanju sprejete objave Odstrani Avtomatsko @@ -283,6 +303,7 @@ Prijavi se s privatnim ključem, za prikaz skritih besed in stavkov Napaka "Ustvaril/a %1$s" "Slika nagradne značke za %1$s" + \"Slika nagradne značke Prejel/a si novo nagradno značko Nagradna zančka podeljena Tekst zapiska kopiran v odložišče @@ -308,6 +329,7 @@ Prijavi se s privatnim ključem, za prikaz skritih besed in stavkov Prenehaj slediti Sledi Izbris iz galerije + Odstrani ta medij iz svoje galerije Prošnja za izbris Amethyst bo poslal prošnjo za izbris zapiska vsem relejem na katere ste povezani. Nobenega jamstva ni, da bo vaš zapisek trajno izbrisan iz teh relejev ali iz drugih relejev, kjer je morda shranjen. Blokiraj @@ -429,6 +451,8 @@ Prijavi se s privatnim ključem, za prikaz skritih besed in stavkov Ne Spisek komu sledimo Vse, čemur sledimo + Vsi sledeni sledenih + Sledi preko posrednika V moji okolici Globalno Spisek utišanih @@ -568,7 +592,10 @@ Prijavi se s privatnim ključem, za prikaz skritih besed in stavkov Releji Sledi tropu Branje + Algoritemski viri Tržnica + Prenos v živo + Skupnosti Pogovori Odobrene objave Ta skupina nima opisa ali pravil. Obrnite se na lastnika, da jih doda @@ -576,6 +603,7 @@ Prijavi se s privatnim ključem, za prikaz skritih besed in stavkov Občutljiva vsebina Doda opozorilo o občutljivi vsebini pred prikazom te vsebine Nastavitve aplikacije + Nastavitve uporabnika Nastavitve Vedno Samo Wifi @@ -620,6 +648,8 @@ Prijavi se s privatnim ključem, za prikaz skritih besed in stavkov Doda Geohash vaše lokacije k objavi. Javnost bo vedela, da se nahajate v radiju 5 km (3 milj) od trenutne lokacije Lokacijsko-ekskluzivna objava To bodo videli samo sledilci lokacije. Vaši splošni sledilci tega ne bodo videli. + Hashtag-ekskluzivna objava + Vidno bo le sledilcem tega hashtaga. Tvoji splošni sledilci tega ne bojo videli. Nalaganje lokacije Brez dovoljenj za lokacijo Doda opozorilo o občutljivi vsebini pred prikazom vaše vsebine. To je primerno za vsebino NSFW ali vsebino, ki jo nekateri lahko smatrajo za žaljivo ali vznemirjajočo @@ -640,6 +670,8 @@ Prijavi se s privatnim ključem, za prikaz skritih besed in stavkov Razlaga članom Spreminjanje imena za dosego novih ciljev. Prilepi iz odložišča + Napačen NIP-47 URI + URI %1$s ni veljaven NIP-47 vpis URI. Za vmesnik aplikacije Temna, svetla ali sistemska tema Samodejno naloži slike in GIF-e @@ -657,6 +689,9 @@ Prijavi se s privatnim ključem, za prikaz skritih besed in stavkov Medij dodan v galerijo profila Ustvarjeno ob Pravila + O nas + Smernice + Moderatorji Prijavi se z Amber Posodobi svoj status Napaka pri razčlembi sporočila o napaki @@ -694,6 +729,7 @@ Prijavi se s privatnim ključem, za prikaz skritih besed in stavkov Denarnica %1$s Napaka pri odpiranju aplikacije za podpisovanje Aplikacije za podpisovanje ni bilo mogoče najti. Preverite, ali aplikacija morda, ni bila odstranjena + Zahteva za vpis zavrnjena Podpisovanje je bilo zavrnjeno Prepričajte se, da je aplikacija za podpisovanje odobrila to transakcijo Denarnica za plačilo z \"lightning\" ni najdena (Napaka: %1$s). Prosimo, namestite \"lightning\" denarnico da boste lahko Zap-ali ostale @@ -822,6 +858,7 @@ Prijavi se s privatnim ključem, za prikaz skritih besed in stavkov Novi kratki mediji: slike ali posnetki Novo zapisek skupnosti Nov produkt + Nova geo-ekskluzivna objava Odpri vse odzive na to objavo Zapri vse odzive na to objavo Odgovori @@ -868,21 +905,41 @@ Prijavi se s privatnim ključem, za prikaz skritih besed in stavkov Naloži ZS Nastavitve relejev Javni za odhajajočo pošto/domači releji + Uporabnik pošilja objave na te releje Ta tip releja shranjuje vse vaše vsebine. Amethyst bo sem pošiljal vaše objave, ostali uporabniki pa bodo te releje uporabljali za iskanje vaših vsebin. Vstavite 1–3 releje. To so lahko osebni releji, plačljivi releji ali javni releji. Javni releji za dospelo pošto + Uporabnik prejema obvestila na teh relejih Ta tip releja sprejema vsa odgovore, komentarje, všečke in zap-e na vaše objave. Ti releji so lahko plačljivi ali brezplačni. Omejitve, ki jih nastavi upravljavec releja, lahko omejijo obvestila, ki jih prejemate, tako v dobrem kot v slabem. Na primer, če ste tarča napada s spam komentarji, ki so nezaželeni, lahko plačljivi releji te nezaželene komentarje odstranijo. Vnesite med 1–3 releje. Releji predala zasebnih sporočil + Uporabnik prejema ZS prek teh relejev Vnesite 1–3 releje, ki bodo služili kot vaš predal za zasebna sporočila. Drugi bodo te releje uporabljali za pošiljanje zasebnih sporočil vam. Releji za zasebna sporočila bi morali sprejeti katero koli sporočilo od kogar koli, vendar samo vam dovoliti njihov prenos. Dobre možnosti so:\n - inbox.nostr.wine (plačljiv)\n - auth.nostr1.com (brezplačen)\n - you.nostr1.com (osebni releji - plačljivi) Zasebni domači releji Vnesite 1–3 releje za shranjevanje dogodkov, ki jih nihče drug ne more videti, kot so vaši osnutki in/ali nastavitve aplikacije. Idealno je, da so ti releji bodisi lokalni ali zahtevajo preverjanje pristnosti pred prenosom vsebine posameznega uporabnika. Splošni releji Amethyst uporablja te releje za prenos objav za vas. + Povezani releji + Trenutni seznam relejev v uporabi Priporočljivi releji Dodajte naslednje releje na svoj seznam Splošnih relejev, da boste prejeli objave od navedenih uporabnikov. Iskalni releji Seznam relejev za uporabo pri iskanju vsebine ali uporabnikov. Označevanje in iskanje ne bosta delovala, če možnosti niso na voljo. Prepričajte se, da podpirajo NIP-50. Lokalni releji Seznam relejev, ki delujejo na tej napravi. + Zaupanja vredni releji + Zaupanja vredni releji + Releji katerim zaupaš in zanje ne rabiš Tor povezave + Proxy releji + Proxy releji + Agregatorji, ki jih mora aplikacija uporabljati za prenos vašega vira vsebin, na primer filter.nostr.wine. To nadomesti model odhodne pošte (outbox model) in bo prisililo aplikacijo k povezovanju prek relejev iz vašega seznama. + Oddajni releji + Oddajni releji + Releji specializirani za posredovanje vaših zapiskov na vse ostale releje kot npr. sendit.nosflare.com. Amethyst bo dodal ta rele k vsem vašim novim dogodkom. + Indeksni releji + Indeksni releji + Releji, specializirani za gostovanje metapodatkov in seznamov relejev vseh uporabnikov, na primer purplepag.es. Amethyst bo te releje uporabil za iskanje uporabnikov, ki niso na vaših seznamih. + Blokirani releji + Blokirani releji + Amethyst se ne bo nikoli povezal na te releje Zap-ni ustvarjalce! Vaša donacija nam pomaga narediti spremembo. Vsak satoši šteje! Doniraj sedaj @@ -972,7 +1029,22 @@ Prijavi se s privatnim ključem, za prikaz skritih besed in stavkov Izberite seznam za filtriranje vira Odjava ob zaklepu naprave Zasebno sporočilo + Javno sporočilo Rele za klepet Rele, na katerega so povezani vsi uporabniki tega klepeta Deli sliko… + Deljenje slike ni uspelo, prosimo poskusite znova… + Išči hashtag: #%1$s + Ne prevajaj iz + Jeziki, ki so prikazani tukaj, ne bodo prevedeni. Izberite jezik, da ga odstranite in ga znova prevedete. + Premor + Predvajaj + Odpri spustni meni + Možnost %1$s od %2$s + Filter vira, %1$s izbran + Filter vira, %1$s + Najdeno je poročilo o napaki + Želite poslati poročilo o napaki Amethyst-u preko zasebnega sporočila? Vaši osebni podatki NE bojo posredovani. + Pošlji + To sporočilo bo izginilo čez %1$d dni diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/EventVerifySerializer.kt b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/EventVerifySerializer.kt new file mode 100644 index 000000000..6fa96f183 --- /dev/null +++ b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/EventVerifySerializer.kt @@ -0,0 +1,58 @@ +/** + * 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.quartz.benchmark + +import androidx.benchmark.junit4.BenchmarkRule +import androidx.benchmark.junit4.measureRepeated +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.crypto.EventHasher +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Benchmark, which will execute on an Android device. + * + * The body of [BenchmarkRule.measureRepeated] is measured in a loop, and Studio will output the + * result. Modify your code to see how it affects performance. + */ +@RunWith(AndroidJUnit4::class) +class EventVerifySerializer { + @get:Rule + val benchmarkRule = BenchmarkRule() + + @Test + fun serializeToCheckId() { + val event = Event.fromJson(largeKind1Event) + benchmarkRule.measureRepeated { + EventHasher.makeJsonForId(event.pubKey, event.createdAt, event.kind, event.tags, event.content) + } + } + + @Test + fun fastSerializeToCheckId() { + val event = Event.fromJson(largeKind1Event) + benchmarkRule.measureRepeated { + EventHasher.fastMakeJsonForId(event.pubKey, event.createdAt, event.kind, event.tags, event.content) + } + } +} diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/HexBenchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/HexBenchmark.kt index bb1272d4e..e4553ade8 100644 --- a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/HexBenchmark.kt +++ b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/HexBenchmark.kt @@ -43,6 +43,13 @@ class HexBenchmark { fr.acinq.secp256k1.Hex .decode(hex) + @Test + fun hexIsEqual() { + r.measureRepeated { + assert(Hex.isEqual(hex, bytes)) + } + } + @Test fun hexDecodeOurs() { r.measureRepeated { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 535ed3a99..01ae64a44 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] accompanistAdaptive = "0.37.3" -activityCompose = "1.10.1" +activityCompose = "1.11.0" agp = "8.13.0" android-compileSdk = "36" android-minSdk = "26" @@ -9,25 +9,25 @@ androidKotlinGeohash = "b481c6a64e" androidxJunit = "1.3.0" appcompat = "1.7.1" audiowaveform = "1.1.2" -benchmark = "1.4.0" -benchmarkJunit4 = "1.4.0" +benchmark = "1.4.1" +benchmarkJunit4 = "1.4.1" biometricKtx = "1.2.0-alpha05" coil = "3.3.0" -composeBom = "2025.08.01" +composeBom = "2025.09.00" coreKtx = "1.17.0" datastore = "1.1.7" espressoCore = "3.7.0" firebaseBom = "34.2.0" fragmentKtx = "1.8.9" gms = "4.4.3" -jacksonModuleKotlin = "2.19.2" +jacksonModuleKotlin = "2.20.0" jna = "5.17.0" jtorctl = "0.4.5.7" junit = "4.13.2" -kotlin = "2.2.10" +kotlin = "2.2.20" kotlinxCollectionsImmutable = "0.4.0" kotlinxSerialization = "1.9.0" -kotlinxSerializationPlugin = "2.2.10" +kotlinxSerializationPlugin = "2.2.20" languageId = "17.0.6" lazysodiumAndroid = "5.2.0" lifecycleRuntimeKtx = "2.9.3" @@ -36,7 +36,7 @@ markdown = "e1151c8" media3 = "1.8.0" mockk = "1.14.5" kotlinx-coroutines-test = "1.10.2" -navigationCompose = "2.9.3" +navigationCompose = "2.9.4" okhttp = "5.1.0" runner = "1.7.0" rfc3986 = "0.1.2" @@ -53,7 +53,7 @@ zoomable = "2.8.1" zxing = "3.5.3" zxingAndroidEmbedded = "4.3.0" windowCoreAndroid = "1.4.0" -androidxCamera = "1.4.2" +androidxCamera = "1.5.0" kotlinStdlib = "2.2.10" kotlinTest = "2.2.10" core = "1.7.0" diff --git a/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/EventSigCheck.kt b/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/EventSigCheck.kt index 23a80683b..cb46c720d 100644 --- a/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/EventSigCheck.kt +++ b/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/EventSigCheck.kt @@ -21,7 +21,14 @@ package com.vitorpamplona.quartz.nip01Core import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.vitorpamplona.quartz.EventFactory +import com.vitorpamplona.quartz.nip01Core.crypto.EventHasher import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper +import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent +import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent +import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertTrue import org.junit.Test import org.junit.runner.RunWith @@ -58,4 +65,98 @@ class EventSigCheck { // Should pass event.checkSignature() } + + @Test + fun checkSerializationWithEmojis() { + val event = + EventFactory.create( + id = "ac2fb0c9b72a6fefe60262fbce6eb8740380b7f964200cb8efdd2e72fcb1ddb0", + pubKey = "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c", + createdAt = 1690288395, + kind = 7, + tags = + arrayOf( + arrayOf("e", "2cc5b6e012c5a64fcf580fc1b53bed25ed0d7f785fd896744524b1a114dcc86e"), + arrayOf("p", "c80b5248fbe8f392bc3ba45091fb4e6e2b5872387601bf90f53992366b30d720"), + ), + content = "🚀", + sig = "6df6aeef98c21a0508a788cbe6a2a6825e5cc69e57dd843dc6e6d4ce2c28dd042947723fda3d8b24489305d86b1e81f9f66b390d6f7dabf9742cd3775e17e53f", + ) + + val old = EventHasher.makeJsonForId(event.pubKey, event.createdAt, event.kind, event.tags, event.content) + val new = EventHasher.fastMakeJsonForId(event.pubKey, event.createdAt, event.kind, event.tags, event.content) + + assertEquals(old.toByteArray().joinToString(), new.joinToString()) + assertTrue(event.verifySignature()) + assertTrue(event.verifyId()) + } + + @Test + fun checkSerializationWithUnicode() { + val event = + EventFactory.create( + id = "e395a1a4cad5b9cf930533c48c2dd175a40331a1d33af8f11b776a0970bc1446", + pubKey = "3fa2504f693c7f0fe71bded634339ae5383a8b14b4164d7c4935830b048dce12", + createdAt = 1707335824, + kind = 30402, + tags = + arrayOf( + arrayOf("d", "b5dd96da-eb92-463a-9cfd-70f1c5ca7e64"), + arrayOf("title", "Google Pixel 7A (NUOVO)\n8GB RAM/128GB MEMORIA"), + arrayOf("summary", "📱Google Pixel 7A (NUOVO)\n👉 8GB RAM/128GB MEMORIA \n\nRete : 5G\nColore : Grigio antracite\n\n👤Sistema operativo : GrapheneOS \n\n🔥Riacquista la tua privacy🔥\n\nℹ️Spacchettato solo per modifica ROM.\n\nNessuna app google e servizi google\n\nSolo app opensource \n\nSet-up privacy oriented incluso dedicato in base a vostre esigenze con Profili dedicati personale/lavoro, Vpn e Tor\n\n🔸️Android 14\n🔸️GrapheneOS  ROM !!!!!!!\n🔸️Adattatore incluso Usb c / Usb per passaggio dati\n🔸️Ricarica con connettore USB Type-C \n🔸Dual SIM (nano SIM singola ed eSIM)\n🔸️Schermo da 6.1 pollici\n🔸️Fotocamera da 64 megapixel\n🔸️8 gb memoria ram\n🔸️128 gb memoria interna\n🔸Dimensioni: 155 x 152 x 72.9 mm\n🔸Sblocco con l'impronta tramite sensore di impronte digitali integrato nel display\n🔸️Batteria da 4385 mAh (Ricarica veloce e Ricarica wireless)\n🔸Materiali: Rivestimento in vetro Corning Gorilla Glass 3 resistente ai graffi\n🔸Resistenza all'acqua e alla polvere di grado IP67 \n\n✅️Cover in slicone in omaggio🔥\n\n💶PREZZO💶 \n\n💰380 € ( SOLO IN BITCOIN) \n\n🚚spedizione privacy oriented da Punto di ritiro a Punto di ritiro (SOLO ITALIA) inclusa, solo scrivendo allo Zio il ref: \"Ziophone21\" \n\n🚚Spedizione Full Privacy ( PREMIUM) \nNessun dato da parte utente, per chi necessitasse di questa sped, la differenza si paga a parte.\n\nAccettati pagamenti:\nBTC 🔗\nBTC ⚡️\nBTC💧\nhttps://image.nostr.build/87c79c56f270ca04607bc6d72b21786837f81344a960a3787820dc0c482c6660.jpg#m=image%2Fjpeg&dim=1254x1280&blurhash=%7CWHB--yGi%5E4mR3IVV%40RhIT%25LIpf5t6RjofafofWBMcMxbH%25MbJofofogt7j%5Bt7azWBoeWVj%5BayayD%24RikCt8t8off7j%5DogWBoej%5BWCofayazayoeROV%40j%5DogfloebHa%7DkCj%3Fayj%5Bj%5Bj%5Bayj%5BWVj%40e-fPa%7Dj%5BWVj%40j%5Ba%23j%5B&x=17b7ff98875a6e6d6e6bddee30e0a1c18ccf4281db940ee8b5bcc0736d2137c6"), + arrayOf("price", "1000", "SATS"), + arrayOf("t", "Electronics"), + arrayOf("location", "Brescia Italia"), + arrayOf("publishedAt", "1707335824"), + arrayOf("condition", "like new"), + arrayOf("image", "https://image.nostr.build/87c79c56f270ca04607bc6d72b21786837f81344a960a3787820dc0c482c6660.jpg#m=image%2Fjpeg&dim=1254x1280&blurhash=%7CWHB--yGi%5E4mR3IVV%40RhIT%25LIpf5t6RjofafofWBMcMxbH%25MbJofofogt7j%5Bt7azWBoeWVj%5BayayD%24RikCt8t8off7j%5DogWBoej%5BWCofayazayoeROV%40j%5DogfloebHa%7DkCj%3Fayj%5Bj%5Bj%5Bayj%5BWVj%40e-fPa%7Dj%5BWVj%40j%5Ba%23j%5B&x=17b7ff98875a6e6d6e6bddee30e0a1c18ccf4281db940ee8b5bcc0736d2137c6"), + arrayOf("r", "https://image.nostr.build/87c79c56f270ca04607bc6d72b21786837f81344a960a3787820dc0c482c6660.jpg#m=image%2Fjpeg&dim=1254x1280&blurhash=%7CWHB--yGi%5E4mR3IVV%40RhIT%25LIpf5t6RjofafofWBMcMxbH%25MbJofofogt7j%5Bt7azWBoeWVj%5BayayD%24RikCt8t8off7j%5DogWBoej%5BWCofayazayoeROV%40j%5DogfloebHa%7DkCj%3Fayj%5Bj%5Bj%5Bayj%5BWVj%40e-fPa%7Dj%5BWVj%40j%5Ba%23j%5B&x=17b7ff98875a6e6d6e6bddee30e0a1c18ccf4281db940ee8b5bcc0736d2137c6"), + arrayOf("alt", "Classifieds listing"), + ), + content = "📱Google Pixel 7A (NUOVO)\n👉 8GB RAM/128GB MEMORIA \n\nRete : 5G\nColore : Grigio antracite\n\n👤Sistema operativo : GrapheneOS \n\n🔥Riacquista la tua privacy🔥\n\nℹ️Spacchettato solo per modifica ROM.\n\nNessuna app google e servizi google\n\nSolo app opensource \n\nSet-up privacy oriented incluso dedicato in base a vostre esigenze con Profili dedicati personale/lavoro, Vpn e Tor\n\n🔸️Android 14\n🔸️GrapheneOS  ROM !!!!!!!\n🔸️Adattatore incluso Usb c / Usb per passaggio dati\n🔸️Ricarica con connettore USB Type-C \n🔸Dual SIM (nano SIM singola ed eSIM)\n🔸️Schermo da 6.1 pollici\n🔸️Fotocamera da 64 megapixel\n🔸️8 gb memoria ram\n🔸️128 gb memoria interna\n🔸Dimensioni: 155 x 152 x 72.9 mm\n🔸Sblocco con l'impronta tramite sensore di impronte digitali integrato nel display\n🔸️Batteria da 4385 mAh (Ricarica veloce e Ricarica wireless)\n🔸Materiali: Rivestimento in vetro Corning Gorilla Glass 3 resistente ai graffi\n🔸Resistenza all'acqua e alla polvere di grado IP67 \n\n✅️Cover in slicone in omaggio🔥\n\n💶PREZZO💶 \n\n💰380 € ( SOLO IN BITCOIN) \n\n🚚spedizione privacy oriented da Punto di ritiro a Punto di ritiro (SOLO ITALIA) inclusa, solo scrivendo allo Zio il ref: \"Ziophone21\" \n\n🚚Spedizione Full Privacy ( PREMIUM) \nNessun dato da parte utente, per chi necessitasse di questa sped, la differenza si paga a parte.\n\nAccettati pagamenti:\nBTC 🔗\nBTC ⚡️\nBTC💧\nhttps://image.nostr.build/87c79c56f270ca04607bc6d72b21786837f81344a960a3787820dc0c482c6660.jpg#m=image%2Fjpeg&dim=1254x1280&blurhash=%7CWHB--yGi%5E4mR3IVV%40RhIT%25LIpf5t6RjofafofWBMcMxbH%25MbJofofogt7j%5Bt7azWBoeWVj%5BayayD%24RikCt8t8off7j%5DogWBoej%5BWCofayazayoeROV%40j%5DogfloebHa%7DkCj%3Fayj%5Bj%5Bj%5Bayj%5BWVj%40e-fPa%7Dj%5BWVj%40j%5Ba%23j%5B&x=17b7ff98875a6e6d6e6bddee30e0a1c18ccf4281db940ee8b5bcc0736d2137c6", + sig = "e03c81ec2543e777583bfcf93c0d82d3055a47a541b1a961db010ed2390e56ddc59e4f891505672757b7ac0d52e891854fdd61f1a4e13b4e6aa563e94cb9368e", + ) + + val old = EventHasher.makeJsonForId(event.pubKey, event.createdAt, event.kind, event.tags, event.content) + val new = EventHasher.fastMakeJsonForId(event.pubKey, event.createdAt, event.kind, event.tags, event.content) + + println(old) + println(String(new)) + + assertEquals(old, String(new)) + assertTrue(event.verifySignature()) + assertTrue(event.verifyId()) + } + + @Test + fun checkSerializationLargeZap() { + val event = + EventFactory.create( + id = "e7f09fddf39fc6cb604708b6af7b4d4adbb07b412847ebb004064040fe8c4b1e", + pubKey = "79f00d3f5a19ec806189fcab03c1be4ff81d18ee4f653c88fac41fe03570f432", + createdAt = 1728921077, + kind = 9735, + tags = + arrayOf( + arrayOf("p", "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"), + arrayOf("e", "58f22b06a219f92fb535c71f7098e076751cf2798208acabbbdefbcb950585f5"), + arrayOf("P", "21335073401a310cc9179fe3a77e9666710cfdf630dfd840f972c183a244b1ad"), + arrayOf("bolt11", "lnbc420n1pns600jdqjfah8wctjvss0p8at5ynp4qtfc238rdkzsj26waa3l8zgag9damzltzsqcrlscj9gvpc7ch2qs2pp5ks03qwm8laa78hnh0xy0p78l6wmyuj3zfqas3gzc2a52lj2zrmtqsp539528j3tvvfzpk8n5v966zccvj3pq2l3etxqxh5qsp8emh29yw3q9qyysgqcqpcxqyz5vqrzjqvdnqyc82a9maxu6c7mee0shqr33u4z9z04wpdwhf96gxzpln8jcrapyqqqqqqp2rcqqqqlgqqqqqzsq2qrzjqw9fu4j39mycmg440ztkraa03u5qhtuc5zfgydsv6ml38qd4azymlapyqqqqqqqp9sqqqqlgqqqq86qqjqrzjq26922n6s5n5undqrf78rjjhgpcczafws45tx8237y7pzx3fg8wwxrgayyqq2mgqqqqqqqqqqqqqqqqq2quc90y7tgfxuauh0vjfvhxjgektaycfesne76jcuk4u9mt6a9l39pddzk3muwy03sjvfk0w8390xxnpzu2656jf2l73ya59ye2yx9aaqpdgxmaq"), + arrayOf("preimage", "0718bf6ba9f503917a2568dac7fe7a5b9361eeb708cd5f57b700060a173ed652"), + arrayOf("description", "{\"id\":\"1110d1216fff3200ed562439556be508a42e4202f66b55f09c9abeed06307d57\",\"pubkey\":\"21335073401a310cc9179fe3a77e9666710cfdf630dfd840f972c183a244b1ad\",\"created_at\":1728921073,\"kind\":9734,\"tags\":[[\"p\",\"460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c\"],[\"e\",\"58f22b06a219f92fb535c71f7098e076751cf2798208acabbbdefbcb950585f5\"],[\"amount\",\"42000\"],[\"relays\",\"wss://christpill.nostr1.com\",\"wss://relay.nostr.bg\",\"wss://relay.noderunners.network\",\"wss://relay.nostrplebs.com\",\"wss://relay.utxo.one\",\"wss://filter.nostr.wine\",\"wss://pyramid.fiatjaf.com\",\"wss://wot.utxo.one\",\"wss://relay.mostr.pub\",\"wss://relay.nostr.band\",\"wss://relay.primal.net\",\"wss://relay.snort.social\",\"wss://purplepag.es\",\"wss://wot.nostr.party\",\"wss://catstrr.swarmstr.com\",\"wss://nostr-relay.derekross.me\",\"wss://relay.damus.io\",\"wss://nos.lol\",\"wss://nostr.wine\",\"wss://relay.bitcoinpark.com\",\"wss://sendit.nosflare.com\",\"wss://nostrelites.org\",\"wss://relay.momostr.pink\",\"ws://localhost:4869\"]],\"content\":\"Onward 🫡\",\"sig\":\"82333a70103b6541f125ac64379a32ad0e9a51eb2769cf899c88df8406bdfa339693d530e640d1c701f4d9d7d847806e1ee2118655b7ba3f9795ce56747eff1b\"}"), + ), + content = "Onward \uD83E\uDEE1", + sig = "81d08765524bd8b774585a57c76d25b1d2c09eaa23efcb075226ba8b64f42e3d9d9403e5edf888e0e58702a24abc084259e21b97e24a275021c5e36186c65f0c", + ) + + val old = EventHasher.makeJsonForId(event.pubKey, event.createdAt, event.kind, event.tags, event.content) + val new = EventHasher.fastMakeJsonForId(event.pubKey, event.createdAt, event.kind, event.tags, event.content) + + println(old) + println(String(new)) + + assertEquals(old.toByteArray().joinToString(), new.joinToString()) + assertTrue(event.verifySignature()) + assertTrue(event.verifyId()) + } } diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/EventExt.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/EventExt.kt index 81e828a65..a5cd423e0 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/EventExt.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/EventExt.kt @@ -30,7 +30,7 @@ fun Event.generateId(): String = EventHasher.hashId(pubKey, createdAt, kind, tag fun Event.verifyId(): Boolean { if (id.isEmpty()) return false - return id == generateId() + return EventHasher.hashIdEquals(id, pubKey, createdAt, kind, tags, content) } fun Event.verifySignature(): Boolean { diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/crypto/EventHasher.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/crypto/EventHasher.kt index 5fcfd9e8c..46af39a31 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/crypto/EventHasher.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/crypto/EventHasher.kt @@ -20,12 +20,20 @@ */ package com.vitorpamplona.quartz.nip01Core.crypto +import com.fasterxml.jackson.core.JsonEncoding +import com.fasterxml.jackson.core.JsonGenerator +import com.fasterxml.jackson.core.JsonProcessingException +import com.fasterxml.jackson.core.util.BufferRecycler +import com.fasterxml.jackson.core.util.ByteArrayBuilder +import com.fasterxml.jackson.databind.JsonMappingException import com.fasterxml.jackson.databind.node.ArrayNode import com.fasterxml.jackson.databind.node.JsonNodeFactory import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper +import com.vitorpamplona.quartz.utils.Hex import com.vitorpamplona.quartz.utils.sha256.sha256 +import java.io.IOException class EventHasher { companion object { @@ -63,13 +71,58 @@ class EventHasher { content: String, ): String = JsonMapper.toJson(makeJsonObjectForId(pubKey, createdAt, kind, tags, content)) + fun fastMakeJsonForId( + pubKey: HexKey, + createdAt: Long, + kind: Int, + tags: Array>, + content: String, + ): ByteArray { + val br: BufferRecycler = JsonMapper.mapper.factory._getBufferRecycler() + try { + ByteArrayBuilder(br).use { bb -> + val generator = JsonMapper.mapper.createGenerator(bb, JsonEncoding.UTF8) + generator.enable(JsonGenerator.Feature.COMBINE_UNICODE_SURROGATES_IN_UTF8) + generator.use { + it.writeStartArray() + it.writeNumber(0) + it.writeString(pubKey) + it.writeNumber(createdAt) + it.writeNumber(kind) + it.writeStartArray() + tags.forEach { tag -> + it.writeStartArray() + tag.forEach { value -> + it.writeString(value) + } + it.writeEndArray() + } + it.writeEndArray() + it.writeString(content) + it.writeEndArray() + } + + val result = bb.toByteArray() + bb.release() + return result + } + } catch (e: JsonProcessingException) { + throw e + } catch (e: IOException) { + // shouldn't really happen, but is declared as possibility so: + throw JsonMappingException.fromUnexpectedIOE(e) + } finally { + br.releaseToPool() + } + } + fun hashIdBytes( pubKey: HexKey, createdAt: Long, kind: Int, tags: Array>, content: String, - ): ByteArray = sha256(makeJsonForId(pubKey, createdAt, kind, tags, content).toByteArray()) + ): ByteArray = sha256(fastMakeJsonForId(pubKey, createdAt, kind, tags, content)) fun hashId(serializedJsonAsBytes: ByteArray): String = sha256(serializedJsonAsBytes).toHexKey() @@ -80,5 +133,17 @@ class EventHasher { tags: Array>, content: String, ): String = hashIdBytes(pubKey, createdAt, kind, tags, content).toHexKey() + + fun hashIdEquals( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + kind: Int, + tags: Array>, + content: String, + ): Boolean { + val outId = hashIdBytes(pubKey, createdAt, kind, tags, content) + return Hex.isEqual(id, outId) + } } } diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/stats/RelayStats.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/stats/RelayStats.kt index a282c9e49..4f1ee25e7 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/stats/RelayStats.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/stats/RelayStats.kt @@ -25,7 +25,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl object RelayStats { private val innerCache = - object : LruCache(100) { + object : LruCache(1000) { override fun create(key: NormalizedRelayUrl?) = RelayStat() } diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip13Pow/miner/PoWMiner.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip13Pow/miner/PoWMiner.kt index 7dd033dcd..22b4ca190 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip13Pow/miner/PoWMiner.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip13Pow/miner/PoWMiner.kt @@ -79,13 +79,13 @@ class PoWMiner( val bytes = EventHasher - .makeJsonForId( - pubKey, - template.createdAt, - template.kind, - template.tags + PoWTag.assemble(initialNonce, desiredPoW), - template.content, - ).toByteArray() + .fastMakeJsonForId( + pubKey = pubKey, + createdAt = template.createdAt, + kind = template.kind, + tags = template.tags + PoWTag.assemble(initialNonce, desiredPoW), + content = template.content, + ) val startIndex = bytes.indexOf(initialNonce.toByteArray()) diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelCreateEvent.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelCreateEvent.kt index b43d0e8ab..e5ad9f340 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelCreateEvent.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelCreateEvent.kt @@ -23,6 +23,7 @@ package com.vitorpamplona.quartz.nip28PublicChat.admin import android.util.Log import androidx.compose.runtime.Immutable import com.fasterxml.jackson.core.JsonParseException +import com.fasterxml.jackson.databind.exc.MismatchedInputException import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder @@ -68,6 +69,9 @@ class ChannelCreateEvent( } catch (e: JsonParseException) { Log.w("ChannelCreateEvent", "Failure to parse ${this.toJson()}", e) ChannelDataNorm() + } catch (e: MismatchedInputException) { + Log.w("ChannelCreateEvent", "Failure to parse ${this.toJson()}", e) + ChannelDataNorm() } cache = newInfo diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip54Wiki/WikiNoteEvent.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip54Wiki/WikiNoteEvent.kt index a33a6c2ad..085c2cfb5 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip54Wiki/WikiNoteEvent.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip54Wiki/WikiNoteEvent.kt @@ -47,7 +47,6 @@ import com.vitorpamplona.quartz.nip19Bech32.eventIds import com.vitorpamplona.quartz.nip19Bech32.pubKeyHints import com.vitorpamplona.quartz.nip19Bech32.pubKeys import com.vitorpamplona.quartz.nip23LongContent.tags.PublishedAtTag -import com.vitorpamplona.quartz.nip23LongContent.tags.PublishedAtTag.Companion.parse import com.vitorpamplona.quartz.nip31Alts.AltTag import com.vitorpamplona.quartz.nip50Search.SearchableEvent import com.vitorpamplona.quartz.utils.TimeUtils diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip71Video/VideoHorizontalEvent.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip71Video/VideoHorizontalEvent.kt index 39738bb91..4701b216a 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip71Video/VideoHorizontalEvent.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip71Video/VideoHorizontalEvent.kt @@ -27,7 +27,6 @@ import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip01Core.tags.dTags.dTag import com.vitorpamplona.quartz.nip22Comments.RootScope import com.vitorpamplona.quartz.nip31Alts.alt -import com.vitorpamplona.quartz.nip71Video.videoIMetas import com.vitorpamplona.quartz.utils.TimeUtils import java.util.UUID diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/utils/Hex.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/utils/Hex.kt index 270d66a5e..0ff6eec22 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/utils/Hex.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/utils/Hex.kt @@ -74,4 +74,21 @@ object Hex { } return String(out) } + + fun isEqual( + id: String, + ourId: ByteArray, + ): Boolean { + var charIndex = 0 + for (i in 0 until ourId.size) { + val chars = byteToHex[ourId[i].toInt() and 0xFF] + if ( + id[charIndex++] != (chars shr 8).toChar() || + id[charIndex++] != (chars and 0xFF).toChar() + ) { + return false + } + } + return true + } }