gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[taler-taler-android] branch master updated (dc2a707 -> f885557)


From: gnunet
Subject: [taler-taler-android] branch master updated (dc2a707 -> f885557)
Date: Mon, 01 Apr 2024 14:34:25 +0200

This is an automated email from the git hooks/post-receive script.

torsten-grote pushed a change to branch master
in repository taler-android.

    from dc2a707  [wallet] Bump qtart to 0.10.1
     new 4c78c29  [wallet] WIP: observability events
     new c3c7cd0  [wallet] Deserialize observability event to JSON object
     new ccc6b47  [wallet] Improve observability UI and make it globally 
reachable from the toolbar
     new e33bc72  [wallet] Make observability log atomic and improve dialog UI
     new f885557  [wallet] Store logs in non-dev mode and remove event type 
translations

The 5 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../src/main/java/net/taler/wallet/MainActivity.kt |  23 +++
 .../main/java/net/taler/wallet/MainViewModel.kt    |  28 +++-
 .../java/net/taler/wallet/backend/ApiResponse.kt   |   2 +
 .../java/net/taler/wallet/backend/InitResponse.kt  |  43 ++++++
 .../net/taler/wallet/backend/WalletBackendApi.kt   |   8 +
 .../net/taler/wallet/events/ObservabilityDialog.kt | 163 +++++++++++++++++++++
 .../net/taler/wallet/events/ObservabilityEvent.kt  |  66 +++++++++
 .../java/net/taler/wallet/exchanges/Exchanges.kt   |   6 +
 .../res/menu/{exchange_list.xml => global_dev.xml} |  11 +-
 wallet/src/main/res/values/strings.xml             |   6 +
 10 files changed, 349 insertions(+), 7 deletions(-)
 create mode 100644 
wallet/src/main/java/net/taler/wallet/events/ObservabilityDialog.kt
 create mode 100644 
wallet/src/main/java/net/taler/wallet/events/ObservabilityEvent.kt
 copy wallet/src/main/res/menu/{exchange_list.xml => global_dev.xml} (83%)

diff --git a/wallet/src/main/java/net/taler/wallet/MainActivity.kt 
b/wallet/src/main/java/net/taler/wallet/MainActivity.kt
index 5dfd920..80b26a5 100644
--- a/wallet/src/main/java/net/taler/wallet/MainActivity.kt
+++ b/wallet/src/main/java/net/taler/wallet/MainActivity.kt
@@ -25,6 +25,7 @@ import android.content.IntentFilter
 import android.net.Uri
 import android.os.Bundle
 import android.util.Log
+import android.view.Menu
 import android.view.MenuItem
 import android.view.View.GONE
 import android.view.View.INVISIBLE
@@ -66,6 +67,7 @@ import 
net.taler.wallet.HostCardEmulatorService.Companion.MERCHANT_NFC_CONNECTED
 import 
net.taler.wallet.HostCardEmulatorService.Companion.MERCHANT_NFC_DISCONNECTED
 import 
net.taler.wallet.HostCardEmulatorService.Companion.TRIGGER_PAYMENT_ACTION
 import net.taler.wallet.databinding.ActivityMainBinding
+import net.taler.wallet.events.ObservabilityDialog
 import net.taler.wallet.refund.RefundStatus
 import java.io.IOException
 import java.net.HttpURLConnection
@@ -144,6 +146,10 @@ class MainActivity : AppCompatActivity(), 
OnNavigationItemSelectedListener,
         model.networkManager.networkStatus.observe(this) { online ->
             ui.content.offlineBanner.visibility = if (online) GONE else VISIBLE
         }
+
+        model.devMode.observe(this) {
+            invalidateMenu()
+        }
     }
 
     @Deprecated("Deprecated in Java")
@@ -159,6 +165,14 @@ class MainActivity : AppCompatActivity(), 
OnNavigationItemSelectedListener,
         }
     }
 
+    override fun onCreateOptionsMenu(menu: Menu?): Boolean {
+        if (model.devMode.value == true) {
+            menuInflater.inflate(R.menu.global_dev, menu)
+        }
+
+        return super.onCreateOptionsMenu(menu)
+    }
+
     override fun onNavigationItemSelected(item: MenuItem): Boolean {
         when (item.itemId) {
             R.id.nav_home -> nav.navigate(R.id.nav_main)
@@ -168,6 +182,15 @@ class MainActivity : AppCompatActivity(), 
OnNavigationItemSelectedListener,
         return true
     }
 
+    override fun onOptionsItemSelected(item: MenuItem): Boolean {
+        when (item.itemId) {
+            R.id.action_show_logs -> {
+                ObservabilityDialog().show(supportFragmentManager, 
"OBSERVABILITY")
+            }
+        }
+        return super.onOptionsItemSelected(item)
+    }
+
     override fun onDestroy() {
         unregisterReceiver(triggerPaymentReceiver)
         unregisterReceiver(nfcConnectedReceiver)
diff --git a/wallet/src/main/java/net/taler/wallet/MainViewModel.kt 
b/wallet/src/main/java/net/taler/wallet/MainViewModel.kt
index 5903446..0f4b94a 100644
--- a/wallet/src/main/java/net/taler/wallet/MainViewModel.kt
+++ b/wallet/src/main/java/net/taler/wallet/MainViewModel.kt
@@ -24,12 +24,17 @@ import androidx.lifecycle.LiveData
 import androidx.lifecycle.MutableLiveData
 import androidx.lifecycle.viewModelScope
 import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.getAndUpdate
 import kotlinx.coroutines.launch
+import kotlinx.serialization.encodeToString
 import net.taler.common.Amount
 import net.taler.common.AmountParserException
 import net.taler.common.Event
 import net.taler.common.toEvent
 import net.taler.wallet.accounts.AccountManager
+import net.taler.wallet.backend.BackendManager
 import net.taler.wallet.backend.NotificationPayload
 import net.taler.wallet.backend.NotificationReceiver
 import net.taler.wallet.backend.VersionReceiver
@@ -38,6 +43,7 @@ import net.taler.wallet.backend.WalletCoreVersion
 import net.taler.wallet.balances.BalanceManager
 import net.taler.wallet.balances.ScopeInfo
 import net.taler.wallet.deposit.DepositManager
+import net.taler.wallet.events.ObservabilityEvent
 import net.taler.wallet.exchanges.ExchangeManager
 import net.taler.wallet.payment.PaymentManager
 import net.taler.wallet.peer.PeerManager
@@ -48,11 +54,17 @@ import net.taler.wallet.withdraw.WithdrawManager
 import org.json.JSONObject
 
 const val TAG = "taler-wallet"
+const val OBSERVABILITY_LIMIT = 100
 
 private val transactionNotifications = listOf(
     "transaction-state-transition",
 )
 
+private val observabilityNotifications = listOf(
+    "task-observability-event",
+    "request-observability-event",
+)
+
 class MainViewModel(
     app: Application,
 ) : AndroidViewModel(app), VersionReceiver, NotificationReceiver {
@@ -85,6 +97,9 @@ class MainViewModel(
     private val mTransactionsEvent = MutableLiveData<Event<ScopeInfo>>()
     val transactionsEvent: LiveData<Event<ScopeInfo>> = mTransactionsEvent
 
+    private val mObservabilityLog = 
MutableStateFlow<List<ObservabilityEvent>>(emptyList())
+    val observabilityLog: StateFlow<List<ObservabilityEvent>> = 
mObservabilityLog
+
     private val mScanCodeEvent = MutableLiveData<Event<Boolean>>()
     val scanCodeEvent: LiveData<Event<Boolean>> = mScanCodeEvent
 
@@ -97,13 +112,24 @@ class MainViewModel(
 
     override fun onNotificationReceived(payload: NotificationPayload) {
         if (payload.type == "waiting-for-retry") return // ignore ping)
-        Log.i(TAG, "Received notification from wallet-core: $payload")
+
+        val str = BackendManager.json.encodeToString(payload)
+        Log.i(TAG, "Received notification from wallet-core: $str")
 
         // Only update balances when we're told they changed
         if (payload.type == "balance-change") 
viewModelScope.launch(Dispatchers.Main) {
             balanceManager.loadBalances()
         }
 
+        if (payload.type in observabilityNotifications && payload.event != 
null) {
+            mObservabilityLog.getAndUpdate { logs ->
+                logs.takeLast(OBSERVABILITY_LIMIT)
+                    .toMutableList().apply {
+                        add(payload.event)
+                    }
+            }
+        }
+
         if (payload.type in transactionNotifications) 
viewModelScope.launch(Dispatchers.Main) {
             // TODO notification API should give us a currency to update
             // update currently selected transaction list
diff --git a/wallet/src/main/java/net/taler/wallet/backend/ApiResponse.kt 
b/wallet/src/main/java/net/taler/wallet/backend/ApiResponse.kt
index 46eb2f0..def4668 100644
--- a/wallet/src/main/java/net/taler/wallet/backend/ApiResponse.kt
+++ b/wallet/src/main/java/net/taler/wallet/backend/ApiResponse.kt
@@ -19,6 +19,7 @@ package net.taler.wallet.backend
 import kotlinx.serialization.SerialName
 import kotlinx.serialization.Serializable
 import kotlinx.serialization.json.JsonObject
+import net.taler.wallet.events.ObservabilityEvent
 
 @Serializable
 sealed class ApiMessage {
@@ -35,6 +36,7 @@ sealed class ApiMessage {
 data class NotificationPayload(
     val type: String,
     val id: String? = null,
+    val event: ObservabilityEvent? = null,
 )
 
 @Serializable
diff --git a/wallet/src/main/java/net/taler/wallet/backend/InitResponse.kt 
b/wallet/src/main/java/net/taler/wallet/backend/InitResponse.kt
index e9f7fcd..7fe1a6b 100644
--- a/wallet/src/main/java/net/taler/wallet/backend/InitResponse.kt
+++ b/wallet/src/main/java/net/taler/wallet/backend/InitResponse.kt
@@ -17,12 +17,55 @@
 package net.taler.wallet.backend
 
 import kotlinx.serialization.Serializable
+import net.taler.wallet.exchanges.BuiltinExchange
 
 @Serializable
 data class InitResponse(
     val versionInfo: WalletCoreVersion,
 )
 
+@Serializable
+data class WalletRunConfig(
+    val builtin: Builtin? = Builtin(),
+    val testing: Testing? = Testing(),
+    val features: Features? = Features(),
+) {
+    /**
+     * Initialization values useful for a complete startup.
+     *
+     * These are values may be overridden by different wallets
+     */
+    @Serializable
+    data class Builtin(
+        val exchanges: List<BuiltinExchange> = emptyList(),
+    )
+
+    /**
+     * Unsafe options which it should only be used to create
+     * testing environment.
+     */
+    @Serializable
+    data class Testing(
+        /**
+         * Allow withdrawal of denominations even though they are about to 
expire.
+         */
+        val denomselAllowLate: Boolean = false,
+        val devModeActive: Boolean = false,
+        val insecureTrustExchange: Boolean = false,
+        val preventThrottling: Boolean = false,
+        val skipDefaults: Boolean = false,
+        val emitObservabilityEvents: Boolean? = false,
+    )
+
+    /**
+     * Configurations values that may be safe to show to the user
+     */
+    @Serializable
+    data class Features(
+        val allowHttp: Boolean = false,
+    )
+}
+
 fun interface VersionReceiver {
     fun onVersionReceived(versionInfo: WalletCoreVersion)
 }
diff --git a/wallet/src/main/java/net/taler/wallet/backend/WalletBackendApi.kt 
b/wallet/src/main/java/net/taler/wallet/backend/WalletBackendApi.kt
index 4e179bb..0619a4e 100644
--- a/wallet/src/main/java/net/taler/wallet/backend/WalletBackendApi.kt
+++ b/wallet/src/main/java/net/taler/wallet/backend/WalletBackendApi.kt
@@ -23,11 +23,13 @@ import kotlinx.coroutines.GlobalScope
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
 import kotlinx.serialization.KSerializer
+import kotlinx.serialization.encodeToString
 import kotlinx.serialization.json.JsonObject
 import kotlinx.serialization.json.decodeFromJsonElement
 import net.taler.wallet.backend.TalerErrorCode.NONE
 import org.json.JSONObject
 import java.io.File
+import net.taler.wallet.backend.WalletRunConfig.*
 
 private const val WALLET_DB = "talerwalletdb.sqlite3"
 
@@ -54,9 +56,15 @@ class WalletBackendApi(
         } else {
             "${app.filesDir}/${WALLET_DB}"
         }
+
+        val config = WalletRunConfig(testing = Testing(
+            emitObservabilityEvents = true,
+        ))
+
         request("init", InitResponse.serializer()) {
             put("persistentStoragePath", db)
             put("logLevel", "INFO")
+            put("config", 
JSONObject(BackendManager.json.encodeToString(config)))
         }.onSuccess { response ->
             versionReceiver.onVersionReceived(response.versionInfo)
         }.onError { error ->
diff --git 
a/wallet/src/main/java/net/taler/wallet/events/ObservabilityDialog.kt 
b/wallet/src/main/java/net/taler/wallet/events/ObservabilityDialog.kt
new file mode 100644
index 0000000..0ce5c01
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/events/ObservabilityDialog.kt
@@ -0,0 +1,163 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2024 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under 
the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 
FOR
+ * A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.events
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.ListItem
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.unit.dp
+import androidx.fragment.app.DialogFragment
+import androidx.fragment.app.activityViewModels
+import kotlinx.serialization.ExperimentalSerializationApi
+import kotlinx.serialization.encodeToString
+import kotlinx.serialization.json.Json
+import net.taler.wallet.MainViewModel
+import net.taler.wallet.R
+import net.taler.wallet.compose.CopyToClipboardButton
+import net.taler.wallet.events.ObservabilityDialog.Companion.json
+import java.time.format.DateTimeFormatter
+import java.time.format.FormatStyle
+
+class ObservabilityDialog: DialogFragment() {
+    private val model: MainViewModel by activityViewModels()
+
+    override fun onCreateView(
+        inflater: LayoutInflater,
+        container: ViewGroup?,
+        savedInstanceState: Bundle?
+    ): View = ComposeView(requireContext()).apply {
+        setContent {
+            val events by model.observabilityLog.collectAsState()
+            ObservabilityComposable(events.reversed()) {
+                dismiss()
+            }
+        }
+    }
+
+    companion object {
+        @OptIn(ExperimentalSerializationApi::class)
+        val json = Json {
+            prettyPrint = true
+            prettyPrintIndent = "  "
+        }
+    }
+}
+
+@Composable
+fun ObservabilityComposable(
+    events: List<ObservabilityEvent>,
+    onDismiss: () -> Unit,
+) {
+    var showJson by remember { mutableStateOf(false) }
+
+    AlertDialog(
+        title = { Text(stringResource(R.string.observability_title)) },
+        text = {
+            LazyColumn(modifier = Modifier.fillMaxSize()) {
+                items(events) { event ->
+                    ObservabilityItem(event, showJson)
+                }
+            }
+        },
+        onDismissRequest = onDismiss,
+        dismissButton = {
+            Button(onClick = { showJson = !showJson }) {
+                Text(if (showJson) {
+                    stringResource(R.string.observability_hide_json)
+                } else {
+                    stringResource(R.string.observability_show_json)
+                })
+            }
+        },
+        confirmButton = {
+            TextButton(onClick = onDismiss) {
+                Text(stringResource(R.string.close))
+            }
+        },
+    )
+}
+
+@Composable
+fun ObservabilityItem(
+    event: ObservabilityEvent,
+    showJson: Boolean,
+) {
+    val body = json.encodeToString(event.body)
+    val timestamp = DateTimeFormatter
+        .ofLocalizedDateTime(FormatStyle.MEDIUM)
+        .format(event.timestamp)
+
+    ListItem(
+        modifier = Modifier.fillMaxWidth(),
+        headlineContent = { Text(event.type) },
+        overlineContent = { Text(timestamp) },
+        supportingContent = if (!showJson) null else { ->
+            Column(
+                horizontalAlignment = Alignment.CenterHorizontally,
+            ) {
+                Box(
+                    modifier = Modifier.background(
+                        MaterialTheme.colorScheme.secondaryContainer,
+                        shape = MaterialTheme.shapes.small,
+                    )
+                ) {
+                    Text(
+                        modifier = Modifier
+                            .padding(10.dp)
+                            .fillMaxWidth(),
+                        text = body,
+                        fontFamily = FontFamily.Monospace,
+                        style = MaterialTheme.typography.bodySmall,
+                    )
+                }
+
+                CopyToClipboardButton(
+                    label = "Event",
+                    content = body,
+                    colors = ButtonDefaults.textButtonColors(),
+                )
+            }
+        },
+    )
+}
\ No newline at end of file
diff --git a/wallet/src/main/java/net/taler/wallet/events/ObservabilityEvent.kt 
b/wallet/src/main/java/net/taler/wallet/events/ObservabilityEvent.kt
new file mode 100644
index 0000000..a50cde2
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/events/ObservabilityEvent.kt
@@ -0,0 +1,66 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2024 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under 
the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 
FOR
+ * A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.events
+
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.encoding.Encoder
+import kotlinx.serialization.json.JsonDecoder
+import kotlinx.serialization.json.JsonElement
+import kotlinx.serialization.json.JsonObject
+import kotlinx.serialization.json.jsonObject
+import kotlinx.serialization.json.jsonPrimitive
+import java.time.LocalDateTime
+
+
+@Serializable(with = ObservabilityEventSerializer::class)
+class ObservabilityEvent(
+    val body: JsonObject,
+    val timestamp: LocalDateTime,
+    val type: String,
+)
+
+class ObservabilityEventSerializer: KSerializer<ObservabilityEvent> {
+    private val jsonElementSerializer = JsonElement.serializer()
+
+    override val descriptor: SerialDescriptor
+        get() = jsonElementSerializer.descriptor
+
+    override fun deserialize(decoder: Decoder): ObservabilityEvent {
+        require(decoder is JsonDecoder)
+        val jsonObject = decoder
+            .decodeJsonElement()
+            .jsonObject
+
+        val type = jsonObject["type"]
+            ?.jsonPrimitive
+            ?.content
+            ?: "unknown"
+
+        return ObservabilityEvent(
+            body = jsonObject,
+            timestamp = LocalDateTime.now(),
+            type = type,
+        )
+    }
+
+    override fun serialize(encoder: Encoder, value: ObservabilityEvent) {
+        encoder.encodeSerializableValue(JsonObject.serializer(), value.body)
+    }
+}
\ No newline at end of file
diff --git a/wallet/src/main/java/net/taler/wallet/exchanges/Exchanges.kt 
b/wallet/src/main/java/net/taler/wallet/exchanges/Exchanges.kt
index ce0bd82..0015e1c 100644
--- a/wallet/src/main/java/net/taler/wallet/exchanges/Exchanges.kt
+++ b/wallet/src/main/java/net/taler/wallet/exchanges/Exchanges.kt
@@ -20,6 +20,12 @@ import kotlinx.serialization.Serializable
 import net.taler.wallet.balances.ScopeInfo
 import net.taler.wallet.cleanExchange
 
+@Serializable
+data class BuiltinExchange(
+    val exchangeBaseUrl: String,
+    val currencyHint: String? = null,
+)
+
 @Serializable
 data class ExchangeItem(
     val exchangeBaseUrl: String,
diff --git a/wallet/src/main/res/menu/exchange_list.xml 
b/wallet/src/main/res/menu/global_dev.xml
similarity index 83%
copy from wallet/src/main/res/menu/exchange_list.xml
copy to wallet/src/main/res/menu/global_dev.xml
index 21c028d..d6f73b9 100644
--- a/wallet/src/main/res/menu/exchange_list.xml
+++ b/wallet/src/main/res/menu/global_dev.xml
@@ -16,10 +16,9 @@
 
 <menu xmlns:android="http://schemas.android.com/apk/res/android";
     xmlns:app="http://schemas.android.com/apk/res-auto";>
-
     <item
-        android:id="@+id/action_add_dev_exchanges"
-        android:title="@string/exchange_list_add_dev"
-        app:showAsAction="never" />
-
-</menu>
+        android:id="@+id/action_show_logs"
+        android:title="@string/show_logs"
+        android:icon="@drawable/ic_bug_report"
+        app:showAsAction="ifRoom" />
+</menu>
\ No newline at end of file
diff --git a/wallet/src/main/res/values/strings.xml 
b/wallet/src/main/res/values/strings.xml
index 2ec3d40..9e3c3a6 100644
--- a/wallet/src/main/res/values/strings.xml
+++ b/wallet/src/main/res/values/strings.xml
@@ -271,6 +271,12 @@ GNU Taler is immune against many types of fraud, such as 
phishing of credit card
     <string name="pending_operations_refuse">Refuse Proposal</string>
     <string name="pending_operations_no_action">(no action)</string>
 
+    <!-- Observability -->
+    <string name="show_logs">Show logs</string>
+    <string name="observability_title">Internal event log</string>
+    <string name="observability_show_json">Show JSON</string>
+    <string name="observability_hide_json">Hide JSON</string>
+
     <string name="settings_dev_mode">Developer Mode</string>
     <string name="settings_dev_mode_summary">Shows more information intended 
for debugging</string>
     <string name="settings_withdraw_testkudos">Withdraw TESTKUDOS</string>

-- 
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]