pakstr 0.3.0 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android-template/app/src/main/java/com/pakstr/app/LocalServer.kt +156 -76
- package/android-template/app/src/main/java/com/pakstr/app/MainActivity.kt +9 -4
- package/android-template/app/src/main/java/com/pakstr/app/RuntimeConfig.kt +109 -11
- package/android-template/app/src/main/java/com/pakstr/app/ShellRuntime.kt +1 -28
- package/android-template/app/src/main/java/com/pakstr/app/WebViewController.kt +19 -0
- package/android-template/app/src/main/java/com/pakstr/app/crypto/NostrEventHasher.kt +8 -4
- package/android-template/app/src/main/java/com/pakstr/app/signer/AmberSigner.kt +46 -67
- package/android-template/app/src/main/java/com/pakstr/app/signer/NostrBridge.kt +0 -7
- package/package.json +1 -1
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
package com.pakstr.app
|
|
2
2
|
|
|
3
3
|
import android.content.Context
|
|
4
|
+
import android.util.Log
|
|
5
|
+
|
|
4
6
|
import android.webkit.MimeTypeMap
|
|
7
|
+
|
|
5
8
|
import fi.iki.elonen.NanoHTTPD
|
|
6
9
|
import java.io.IOException
|
|
7
|
-
import java.util.Locale
|
|
8
|
-
|
|
9
10
|
import java.net.HttpURLConnection
|
|
10
11
|
import java.net.URL
|
|
12
|
+
import java.util.Locale
|
|
11
13
|
|
|
12
14
|
class LocalServer(
|
|
13
15
|
|
|
@@ -17,150 +19,228 @@ class LocalServer(
|
|
|
17
19
|
|
|
18
20
|
) : NanoHTTPD(port) {
|
|
19
21
|
|
|
22
|
+
private val TAG = "LocalServer"
|
|
23
|
+
|
|
20
24
|
override fun serve(session: IHTTPSession?): Response {
|
|
21
|
-
// Fallback to root if URI is null
|
|
22
|
-
val uri =
|
|
23
25
|
|
|
24
|
-
|
|
26
|
+
val uri = (session?.uri ?: "/").replace("..", "")
|
|
25
27
|
|
|
26
|
-
.replace("..", "")
|
|
27
28
|
|
|
28
29
|
if (uri.startsWith("/api/")) {
|
|
29
30
|
|
|
30
|
-
|
|
31
|
+
if (!RuntimeConfig.apiEnabled(context)) {
|
|
31
32
|
|
|
32
|
-
|
|
33
|
+
return newFixedLengthResponse(
|
|
33
34
|
|
|
34
|
-
|
|
35
|
+
Response.Status.NOT_FOUND,
|
|
35
36
|
|
|
36
|
-
|
|
37
|
+
"application/json",
|
|
38
|
+
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
{
|
|
42
|
+
"error":"api_disabled"
|
|
43
|
+
}
|
|
44
|
+
""".trimIndent()
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return proxyApi(session, uri)
|
|
37
49
|
|
|
38
50
|
}
|
|
39
51
|
|
|
40
|
-
// Map the root URL to index.html, otherwise target the requested file in the assets folder
|
|
41
52
|
val filePath = when {
|
|
42
53
|
|
|
43
54
|
uri == "/" -> "www/index.html"
|
|
44
55
|
|
|
45
|
-
uri.contains(".") -> "www$uri"
|
|
56
|
+
uri.contains(".") -> "www$uri"
|
|
46
57
|
|
|
47
|
-
else -> "www/index.html"
|
|
58
|
+
else -> "www/index.html"
|
|
48
59
|
|
|
49
60
|
}
|
|
50
61
|
|
|
62
|
+
|
|
51
63
|
return try {
|
|
52
|
-
|
|
64
|
+
|
|
65
|
+
Log.d(
|
|
66
|
+
TAG, "Serving asset: $filePath"
|
|
67
|
+
)
|
|
68
|
+
|
|
53
69
|
val inputStream = context.assets.open(filePath)
|
|
54
70
|
|
|
55
|
-
|
|
56
|
-
|
|
71
|
+
newChunkedResponse(
|
|
72
|
+
Response.Status.OK, getMimeType(filePath), inputStream
|
|
73
|
+
)
|
|
74
|
+
|
|
57
75
|
} catch (e: IOException) {
|
|
58
|
-
|
|
59
|
-
|
|
76
|
+
|
|
77
|
+
Log.e(
|
|
78
|
+
TAG, "Failed loading asset: $filePath", e
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
newFixedLengthResponse(
|
|
82
|
+
Response.Status.NOT_FOUND, "text/plain", "404 Not Found"
|
|
83
|
+
)
|
|
60
84
|
}
|
|
61
85
|
}
|
|
62
86
|
|
|
63
|
-
/**
|
|
64
|
-
* Automatically detects the correct MIME type based on the file extension.
|
|
65
|
-
*/
|
|
66
87
|
private fun getMimeType(
|
|
67
88
|
file: String
|
|
68
89
|
): String {
|
|
69
90
|
|
|
70
|
-
val extension =
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
?.lowercase(Locale.ROOT)
|
|
74
|
-
?: ""
|
|
91
|
+
val extension = MimeTypeMap.getFileExtensionFromUrl(file)
|
|
92
|
+
?.lowercase(Locale.ROOT) ?: ""
|
|
93
|
+
|
|
75
94
|
|
|
76
95
|
return when (extension) {
|
|
77
|
-
"js" ->
|
|
78
|
-
"application/javascript"
|
|
79
96
|
|
|
80
|
-
"
|
|
81
|
-
"application/json"
|
|
97
|
+
"js" -> "application/javascript"
|
|
82
98
|
|
|
83
|
-
"
|
|
84
|
-
"text/css"
|
|
99
|
+
"json" -> "application/json"
|
|
85
100
|
|
|
86
|
-
"
|
|
87
|
-
"text/html"
|
|
101
|
+
"css" -> "text/css"
|
|
88
102
|
|
|
89
|
-
"
|
|
90
|
-
"image/svg+xml"
|
|
103
|
+
"html" -> "text/html"
|
|
91
104
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
?: "application/octet-stream"
|
|
105
|
+
"svg" -> "image/svg+xml"
|
|
106
|
+
|
|
107
|
+
else -> MimeTypeMap.getSingleton()
|
|
108
|
+
.getMimeTypeFromExtension(extension) ?: "application/octet-stream"
|
|
97
109
|
}
|
|
98
110
|
}
|
|
99
111
|
|
|
112
|
+
override fun start() {
|
|
113
|
+
|
|
114
|
+
super.start()
|
|
115
|
+
|
|
116
|
+
Log.d(
|
|
117
|
+
TAG, "🚀 LocalServer started on port $listeningPort"
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
|
|
100
121
|
private fun proxyApi(
|
|
101
|
-
session: IHTTPSession?,
|
|
102
|
-
uri: String
|
|
122
|
+
session: IHTTPSession?, uri: String
|
|
103
123
|
): Response {
|
|
104
124
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
125
|
+
return try {
|
|
126
|
+
|
|
127
|
+
val config = RuntimeConfig.load(context)
|
|
128
|
+
|
|
129
|
+
val apiBase = config.optString("apiBase")
|
|
130
|
+
|
|
131
|
+
if (apiBase.isBlank()) {
|
|
132
|
+
return newFixedLengthResponse(
|
|
108
133
|
Response.Status.INTERNAL_ERROR,
|
|
109
134
|
"application/json",
|
|
110
135
|
"""{"error":"apiBase missing"}"""
|
|
111
136
|
)
|
|
137
|
+
}
|
|
112
138
|
|
|
139
|
+
val finalUrl = apiBase.removeSuffix("/") + uri
|
|
113
140
|
|
|
114
|
-
|
|
141
|
+
val connection = URL(finalUrl).openConnection() as HttpURLConnection
|
|
142
|
+
connection.doInput = true
|
|
143
|
+
connection.instanceFollowRedirects = false
|
|
144
|
+
|
|
145
|
+
val method = session?.method?.name ?: "GET"
|
|
146
|
+
|
|
147
|
+
connection.requestMethod = method
|
|
148
|
+
|
|
149
|
+
connection.connectTimeout = 15000
|
|
150
|
+
connection.readTimeout = 30000
|
|
151
|
+
|
|
152
|
+
/*
|
|
153
|
+
* Forward headers
|
|
154
|
+
*/
|
|
115
155
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
156
|
+
session?.headers?.forEach { (key, value) ->
|
|
157
|
+
|
|
158
|
+
val skip = setOf(
|
|
159
|
+
"host", "content-length", "connection", "accept-encoding"
|
|
119
160
|
)
|
|
120
161
|
|
|
121
162
|
|
|
122
|
-
|
|
123
|
-
url.openConnection() as HttpURLConnection
|
|
163
|
+
if (!skip.contains(key.lowercase())) {
|
|
124
164
|
|
|
165
|
+
connection.setRequestProperty(
|
|
166
|
+
key, value
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
session?.headers?.get("authorization")
|
|
171
|
+
?.let { auth ->
|
|
172
|
+
connection.setRequestProperty(
|
|
173
|
+
"Authorization",
|
|
174
|
+
auth
|
|
175
|
+
)
|
|
176
|
+
}
|
|
125
177
|
|
|
126
|
-
connection.requestMethod =
|
|
127
|
-
session?.method?.name ?: "GET"
|
|
128
178
|
|
|
179
|
+
if (method == "POST" || method == "PUT" || method == "PATCH") {
|
|
129
180
|
|
|
130
|
-
|
|
131
|
-
connection.readTimeout = 10000
|
|
181
|
+
val files = HashMap<String, String>()
|
|
132
182
|
|
|
183
|
+
session?.parseBody(files)
|
|
133
184
|
|
|
134
|
-
|
|
135
|
-
connection.responseCode
|
|
185
|
+
val body = files["postData"] ?: ""
|
|
136
186
|
|
|
187
|
+
val bytes = body.toByteArray(Charsets.UTF_8)
|
|
137
188
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
connection.
|
|
141
|
-
|
|
142
|
-
|
|
189
|
+
connection.doOutput = true
|
|
190
|
+
session?.headers?.get("content-type")?.let {
|
|
191
|
+
connection.setRequestProperty(
|
|
192
|
+
"Content-Type",
|
|
193
|
+
it
|
|
194
|
+
)
|
|
195
|
+
}
|
|
143
196
|
|
|
197
|
+
connection.outputStream.use { output ->
|
|
144
198
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
199
|
+
output.write(bytes)
|
|
200
|
+
output.flush()
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
val code = connection.responseCode
|
|
205
|
+
|
|
206
|
+
val stream = if (code >= 400) connection.errorStream
|
|
207
|
+
else connection.inputStream
|
|
208
|
+
|
|
209
|
+
val body = stream?.bufferedReader()
|
|
210
|
+
?.use {
|
|
211
|
+
it.readText()
|
|
212
|
+
} ?: ""
|
|
151
213
|
|
|
214
|
+
val responseContentType =
|
|
215
|
+
connection.contentType ?: "application/json"
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
connection.disconnect()
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
return newFixedLengthResponse(
|
|
222
|
+
|
|
223
|
+
Response.Status.lookup(code) ?: Response.Status.OK,
|
|
224
|
+
|
|
225
|
+
responseContentType,
|
|
226
|
+
|
|
227
|
+
body
|
|
228
|
+
|
|
229
|
+
)
|
|
152
230
|
|
|
153
231
|
} catch (e: Exception) {
|
|
232
|
+
Log.e(
|
|
233
|
+
TAG,
|
|
234
|
+
"Proxy failed: ${e.javaClass.simpleName}"
|
|
235
|
+
)
|
|
236
|
+
return newFixedLengthResponse(
|
|
154
237
|
|
|
155
|
-
newFixedLengthResponse(
|
|
156
238
|
Response.Status.INTERNAL_ERROR,
|
|
239
|
+
|
|
157
240
|
"application/json",
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
"message":"${e.message}"
|
|
162
|
-
}
|
|
163
|
-
""".trimIndent()
|
|
241
|
+
|
|
242
|
+
"""{"error":"proxy_failed"}""".trimIndent()
|
|
243
|
+
|
|
164
244
|
)
|
|
165
245
|
}
|
|
166
246
|
}
|
|
@@ -18,17 +18,14 @@ import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
|
|
18
18
|
import androidx.core.view.ViewCompat
|
|
19
19
|
import androidx.core.view.WindowInsetsCompat
|
|
20
20
|
import androidx.core.view.updatePadding
|
|
21
|
-
|
|
21
|
+
|
|
22
22
|
import com.pakstr.app.permissions.PermissionManager
|
|
23
23
|
import com.pakstr.app.permissions.interfaces.PermissionHandler
|
|
24
|
-
import com.pakstr.app.crypto.CryptoInitializer
|
|
25
|
-
import com.pakstr.app.debug.AppDebugLogger
|
|
26
24
|
|
|
27
25
|
import com.pakstr.app.signer.SignerManager
|
|
28
26
|
import com.pakstr.app.signer.SignerType
|
|
29
27
|
import com.pakstr.app.utils.AppPreferences
|
|
30
28
|
import com.pakstr.app.utils.SignerPickerBottomSheet
|
|
31
|
-
import kotlinx.coroutines.launch
|
|
32
29
|
|
|
33
30
|
class MainActivity : AppCompatActivity(), PermissionHandler {
|
|
34
31
|
|
|
@@ -41,6 +38,9 @@ class MainActivity : AppCompatActivity(), PermissionHandler {
|
|
|
41
38
|
private lateinit var amberLauncher:
|
|
42
39
|
ActivityResultLauncher<Intent>
|
|
43
40
|
|
|
41
|
+
|
|
42
|
+
private val TAG = "ShellRuntime"
|
|
43
|
+
|
|
44
44
|
override fun onCreate(
|
|
45
45
|
savedInstanceState: Bundle?
|
|
46
46
|
) {
|
|
@@ -93,9 +93,11 @@ class MainActivity : AppCompatActivity(), PermissionHandler {
|
|
|
93
93
|
insets
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
|
|
96
97
|
setupRuntime()
|
|
97
98
|
setupBackNavigation()
|
|
98
99
|
showLoader(true)
|
|
100
|
+
|
|
99
101
|
runtime.start()
|
|
100
102
|
|
|
101
103
|
}
|
|
@@ -143,11 +145,14 @@ class MainActivity : AppCompatActivity(), PermissionHandler {
|
|
|
143
145
|
findViewById(R.id.mainContainer)
|
|
144
146
|
webView =
|
|
145
147
|
findViewById(R.id.webView)
|
|
148
|
+
|
|
146
149
|
loader =
|
|
147
150
|
findViewById(R.id.loader)
|
|
148
151
|
|
|
149
152
|
permissionManager =
|
|
150
153
|
PermissionManager(this)
|
|
154
|
+
|
|
155
|
+
WebView.setWebContentsDebuggingEnabled(true)
|
|
151
156
|
}
|
|
152
157
|
|
|
153
158
|
private fun handleSignerSetup() {
|
|
@@ -1,25 +1,30 @@
|
|
|
1
1
|
package com.pakstr.app
|
|
2
2
|
|
|
3
3
|
import android.content.Context
|
|
4
|
+
import android.util.Log
|
|
4
5
|
import org.json.JSONObject
|
|
5
6
|
|
|
6
7
|
object RuntimeConfig {
|
|
7
8
|
|
|
9
|
+
private const val TAG = "RuntimeConfig"
|
|
10
|
+
|
|
8
11
|
private var config: JSONObject? = null
|
|
9
12
|
|
|
10
13
|
|
|
11
14
|
fun load(context: Context): JSONObject {
|
|
12
15
|
|
|
13
|
-
|
|
14
|
-
|
|
16
|
+
config?.let {
|
|
17
|
+
Log.d(TAG, "Returning cached config: $it")
|
|
18
|
+
return it
|
|
15
19
|
}
|
|
16
20
|
|
|
17
21
|
|
|
18
22
|
config = try {
|
|
19
23
|
|
|
20
24
|
val input =
|
|
21
|
-
context.assets.open(
|
|
22
|
-
|
|
25
|
+
context.assets.open(
|
|
26
|
+
"www/pakstr-runtime.json"
|
|
27
|
+
)
|
|
23
28
|
|
|
24
29
|
val json =
|
|
25
30
|
input.bufferedReader()
|
|
@@ -28,24 +33,117 @@ object RuntimeConfig {
|
|
|
28
33
|
|
|
29
34
|
JSONObject(json)
|
|
30
35
|
|
|
31
|
-
|
|
32
36
|
} catch (e: Exception) {
|
|
33
37
|
|
|
38
|
+
Log.e(
|
|
39
|
+
TAG,
|
|
40
|
+
"Failed loading runtime config",
|
|
41
|
+
e
|
|
42
|
+
)
|
|
43
|
+
|
|
34
44
|
JSONObject()
|
|
35
45
|
|
|
36
46
|
}
|
|
37
47
|
|
|
38
48
|
|
|
49
|
+
Log.d(
|
|
50
|
+
TAG,
|
|
51
|
+
"FINAL CONFIG: $config"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
39
55
|
return config!!
|
|
40
56
|
}
|
|
41
57
|
|
|
42
58
|
|
|
43
|
-
|
|
59
|
+
/**
|
|
60
|
+
* Returns backend API URL.
|
|
61
|
+
*
|
|
62
|
+
* Empty means:
|
|
63
|
+
* - Nostr-only mode
|
|
64
|
+
* - no REST API proxy
|
|
65
|
+
*/
|
|
66
|
+
fun apiBase(
|
|
67
|
+
context: Context
|
|
68
|
+
): String? {
|
|
69
|
+
|
|
70
|
+
val value =
|
|
71
|
+
load(context)
|
|
72
|
+
.optString(
|
|
73
|
+
"apiBase",
|
|
74
|
+
""
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
Log.d(
|
|
79
|
+
TAG,
|
|
80
|
+
"apiBase=$value"
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
return value.takeIf {
|
|
85
|
+
it.isNotBlank()
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Determines if REST API proxy should be enabled.
|
|
92
|
+
*/
|
|
93
|
+
fun apiEnabled(
|
|
94
|
+
context: Context
|
|
95
|
+
): Boolean {
|
|
96
|
+
|
|
97
|
+
val enabled =
|
|
98
|
+
!apiBase(context).isNullOrBlank()
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
Log.d(
|
|
102
|
+
TAG,
|
|
103
|
+
"apiEnabled=$enabled"
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
return enabled
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Nostr relay URL.
|
|
113
|
+
*/
|
|
114
|
+
fun relayUrl(
|
|
115
|
+
context: Context
|
|
116
|
+
): String {
|
|
117
|
+
|
|
118
|
+
val relay =
|
|
119
|
+
load(context)
|
|
120
|
+
.optString(
|
|
121
|
+
"relayUrl",
|
|
122
|
+
""
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
Log.d(
|
|
127
|
+
TAG,
|
|
128
|
+
"relayUrl=$relay"
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
return relay
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Clears cached config.
|
|
138
|
+
* Useful for testing after changing pakstr-runtime.json.
|
|
139
|
+
*/
|
|
140
|
+
fun clearCache() {
|
|
141
|
+
|
|
142
|
+
config = null
|
|
44
143
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
144
|
+
Log.d(
|
|
145
|
+
TAG,
|
|
146
|
+
"Config cache cleared"
|
|
147
|
+
)
|
|
50
148
|
}
|
|
51
149
|
}
|
|
@@ -24,9 +24,6 @@ class ShellRuntime(
|
|
|
24
24
|
|
|
25
25
|
private lateinit var controller: WebViewController
|
|
26
26
|
|
|
27
|
-
private var apiBase: String? = null
|
|
28
|
-
|
|
29
|
-
|
|
30
27
|
fun start() {
|
|
31
28
|
|
|
32
29
|
controller = WebViewController(
|
|
@@ -37,16 +34,6 @@ class ShellRuntime(
|
|
|
37
34
|
permissionHandler
|
|
38
35
|
)
|
|
39
36
|
|
|
40
|
-
val runtimeConfig = RuntimeConfig.load(context)
|
|
41
|
-
|
|
42
|
-
apiBase = runtimeConfig.optString(
|
|
43
|
-
|
|
44
|
-
"apiBase",
|
|
45
|
-
|
|
46
|
-
null
|
|
47
|
-
|
|
48
|
-
)
|
|
49
|
-
|
|
50
37
|
CoroutineScope(Dispatchers.IO).launch {
|
|
51
38
|
|
|
52
39
|
val ok = startServer()
|
|
@@ -80,48 +67,34 @@ class ShellRuntime(
|
|
|
80
67
|
if (server != null)
|
|
81
68
|
return true
|
|
82
69
|
|
|
83
|
-
|
|
84
70
|
return try {
|
|
85
71
|
|
|
86
72
|
server = LocalServer(
|
|
87
|
-
|
|
88
73
|
context,
|
|
89
|
-
|
|
90
|
-
port,
|
|
91
|
-
|
|
92
|
-
apiBase
|
|
93
|
-
|
|
74
|
+
port
|
|
94
75
|
)
|
|
95
76
|
|
|
96
|
-
|
|
97
77
|
server?.start(
|
|
98
78
|
NanoHTTPD.SOCKET_READ_TIMEOUT,
|
|
99
79
|
false
|
|
100
80
|
)
|
|
101
81
|
|
|
102
|
-
|
|
103
82
|
AppDebugLogger.log(
|
|
104
83
|
"SERVER",
|
|
105
84
|
"Started OK"
|
|
106
85
|
)
|
|
107
86
|
|
|
108
|
-
|
|
109
87
|
true
|
|
110
88
|
|
|
111
|
-
|
|
112
89
|
} catch (e: Exception) {
|
|
113
90
|
|
|
114
|
-
|
|
115
91
|
AppDebugLogger.log(
|
|
116
92
|
"SERVER",
|
|
117
93
|
"Failed to start: ${e.message}"
|
|
118
94
|
)
|
|
119
95
|
|
|
120
|
-
|
|
121
96
|
false
|
|
122
|
-
|
|
123
97
|
}
|
|
124
|
-
|
|
125
98
|
}
|
|
126
99
|
|
|
127
100
|
|
|
@@ -49,6 +49,7 @@ class WebViewController(
|
|
|
49
49
|
|
|
50
50
|
fun setup() {
|
|
51
51
|
showLoader()
|
|
52
|
+
setupCookies()
|
|
52
53
|
setupWebView()
|
|
53
54
|
loadApp()
|
|
54
55
|
}
|
|
@@ -141,6 +142,24 @@ class WebViewController(
|
|
|
141
142
|
|
|
142
143
|
}
|
|
143
144
|
|
|
145
|
+
private fun setupCookies() {
|
|
146
|
+
|
|
147
|
+
val cookieManager = CookieManager.getInstance()
|
|
148
|
+
|
|
149
|
+
cookieManager.setAcceptCookie(true)
|
|
150
|
+
|
|
151
|
+
cookieManager.setAcceptThirdPartyCookies(
|
|
152
|
+
|
|
153
|
+
webView,
|
|
154
|
+
|
|
155
|
+
true
|
|
156
|
+
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
cookieManager.flush()
|
|
160
|
+
|
|
161
|
+
}
|
|
162
|
+
|
|
144
163
|
/**
|
|
145
164
|
* Loads the local server URL with a minor delay to ensure NanoHTTPD is fully bound.
|
|
146
165
|
*/
|
|
@@ -34,15 +34,20 @@ object NostrEventHasher {
|
|
|
34
34
|
event: JSONObject
|
|
35
35
|
): String {
|
|
36
36
|
|
|
37
|
+
val pubkey =
|
|
38
|
+
event.optString("pubkey")
|
|
39
|
+
|
|
40
|
+
require(pubkey.isNotBlank()) {
|
|
41
|
+
"Missing pubkey in nostr event: $event"
|
|
42
|
+
}
|
|
43
|
+
|
|
37
44
|
val serialized =
|
|
38
45
|
JSONArray()
|
|
39
46
|
.apply {
|
|
40
47
|
|
|
41
48
|
put(0)
|
|
42
49
|
|
|
43
|
-
put(
|
|
44
|
-
event.getString("pubkey")
|
|
45
|
-
)
|
|
50
|
+
put(pubkey)
|
|
46
51
|
|
|
47
52
|
put(
|
|
48
53
|
event.getLong("created_at")
|
|
@@ -67,7 +72,6 @@ object NostrEventHasher {
|
|
|
67
72
|
return sha256Hex(
|
|
68
73
|
serialized.toByteArray()
|
|
69
74
|
)
|
|
70
|
-
|
|
71
75
|
}
|
|
72
76
|
|
|
73
77
|
/**
|
|
@@ -118,79 +118,80 @@ class AmberSigner(
|
|
|
118
118
|
override suspend fun signEvent(
|
|
119
119
|
eventJson: String
|
|
120
120
|
): String =
|
|
121
|
-
|
|
122
121
|
withTimeout(30_000) {
|
|
123
122
|
|
|
123
|
+
val event = JSONObject(eventJson)
|
|
124
|
+
|
|
125
|
+
if (!event.has("pubkey")) {
|
|
126
|
+
|
|
127
|
+
val pubkey = getPublicKey()
|
|
128
|
+
|
|
129
|
+
require(pubkey.isNotBlank()) {
|
|
130
|
+
"Amber returned empty pubkey"
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
event.put(
|
|
134
|
+
"pubkey",
|
|
135
|
+
pubkey
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
|
|
124
139
|
suspendCancellableCoroutine { continuation ->
|
|
125
140
|
|
|
126
|
-
|
|
141
|
+
AppDebugLogger.log(
|
|
142
|
+
"AMBER",
|
|
143
|
+
"Signing event request"
|
|
144
|
+
)
|
|
127
145
|
|
|
128
146
|
pendingCallback = { signature ->
|
|
129
147
|
|
|
130
148
|
if (signature.isEmpty()) {
|
|
131
149
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
continuation.resume("")
|
|
135
|
-
|
|
136
|
-
}
|
|
150
|
+
continuation.resume("")
|
|
137
151
|
|
|
138
152
|
} else {
|
|
139
153
|
|
|
140
154
|
val signedEvent =
|
|
141
|
-
|
|
142
155
|
NostrEventBuilder.addId(event)
|
|
143
156
|
.apply {
|
|
144
|
-
|
|
145
157
|
put(
|
|
146
|
-
"sig",
|
|
158
|
+
"sig",
|
|
159
|
+
signature
|
|
147
160
|
)
|
|
148
|
-
|
|
149
161
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
} else {
|
|
166
|
-
continuation.resume("")
|
|
167
|
-
}
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
val verified =
|
|
165
|
+
NostrEventVerifier.verify(
|
|
166
|
+
signedEvent
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
if (verified) {
|
|
171
|
+
continuation.resume(
|
|
172
|
+
signedEvent.toString()
|
|
173
|
+
)
|
|
174
|
+
} else {
|
|
175
|
+
continuation.resume("")
|
|
168
176
|
}
|
|
169
177
|
}
|
|
170
178
|
}
|
|
171
179
|
|
|
180
|
+
|
|
172
181
|
val intent = Intent(
|
|
173
|
-
Intent.ACTION_VIEW,
|
|
182
|
+
Intent.ACTION_VIEW,
|
|
183
|
+
Uri.parse(
|
|
174
184
|
"nostrsigner:${Uri.encode(event.toString())}"
|
|
175
185
|
)
|
|
176
186
|
).apply {
|
|
177
187
|
putExtra(
|
|
178
|
-
"type",
|
|
188
|
+
"type",
|
|
189
|
+
"sign_event"
|
|
179
190
|
)
|
|
180
191
|
}
|
|
181
|
-
try {
|
|
182
|
-
launcher.launch(intent)
|
|
183
|
-
|
|
184
|
-
} catch (_: ActivityNotFoundException) {
|
|
185
192
|
|
|
186
|
-
if (continuation.isActive) {
|
|
187
|
-
continuation.resume("")
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
193
|
|
|
191
|
-
|
|
192
|
-
pendingCallback = null
|
|
193
|
-
}
|
|
194
|
+
launcher.launch(intent)
|
|
194
195
|
}
|
|
195
196
|
}
|
|
196
197
|
|
|
@@ -434,13 +435,6 @@ class AmberSigner(
|
|
|
434
435
|
)
|
|
435
436
|
}
|
|
436
437
|
|
|
437
|
-
|
|
438
|
-
AppDebugLogger.log(
|
|
439
|
-
"NIP44",
|
|
440
|
-
"decrypt sender=$pubkey current=$pubkey length=${ciphertext.length}"
|
|
441
|
-
)
|
|
442
|
-
|
|
443
|
-
|
|
444
438
|
try {
|
|
445
439
|
|
|
446
440
|
launcher.launch(intent)
|
|
@@ -489,27 +483,12 @@ class AmberSigner(
|
|
|
489
483
|
?: result.data?.getStringExtra("content")
|
|
490
484
|
|
|
491
485
|
?: ""
|
|
492
|
-
|
|
493
|
-
AppDebugLogger.log(
|
|
494
|
-
|
|
495
|
-
"AMBER_RESULT",
|
|
496
|
-
|
|
497
|
-
value
|
|
498
|
-
|
|
499
|
-
)
|
|
500
486
|
AppDebugLogger.log(
|
|
501
|
-
|
|
502
|
-
"
|
|
503
|
-
|
|
504
|
-
result.data?.extras?.keySet()?.joinToString()
|
|
505
|
-
|
|
506
|
-
?: "NO_KEYS"
|
|
507
|
-
|
|
487
|
+
"AMBER",
|
|
488
|
+
"Operation completed"
|
|
508
489
|
)
|
|
509
490
|
|
|
510
|
-
callback?.invoke(
|
|
511
|
-
value
|
|
512
|
-
)
|
|
491
|
+
callback?.invoke(value)
|
|
513
492
|
}
|
|
514
493
|
|
|
515
494
|
private suspend fun getCachedPublicKey(): String {
|
|
@@ -133,7 +133,6 @@ class NostrBridge(
|
|
|
133
133
|
val plaintext =
|
|
134
134
|
json.getString("plaintext")
|
|
135
135
|
|
|
136
|
-
|
|
137
136
|
val result =
|
|
138
137
|
signerManager
|
|
139
138
|
.getSigner()
|
|
@@ -171,7 +170,6 @@ class NostrBridge(
|
|
|
171
170
|
}
|
|
172
171
|
}
|
|
173
172
|
|
|
174
|
-
|
|
175
173
|
"nip04_decrypt" -> {
|
|
176
174
|
|
|
177
175
|
scope.launch {
|
|
@@ -182,7 +180,6 @@ class NostrBridge(
|
|
|
182
180
|
"NIP04 decrypt payload is empty"
|
|
183
181
|
}
|
|
184
182
|
|
|
185
|
-
|
|
186
183
|
val json =
|
|
187
184
|
JSONObject(payload)
|
|
188
185
|
|
|
@@ -192,7 +189,6 @@ class NostrBridge(
|
|
|
192
189
|
val ciphertext =
|
|
193
190
|
json.getString("ciphertext")
|
|
194
191
|
|
|
195
|
-
|
|
196
192
|
val result =
|
|
197
193
|
signerManager
|
|
198
194
|
.getSigner()
|
|
@@ -207,7 +203,6 @@ class NostrBridge(
|
|
|
207
203
|
result
|
|
208
204
|
)
|
|
209
205
|
|
|
210
|
-
|
|
211
206
|
} catch (e: SignerUnavailableException) {
|
|
212
207
|
|
|
213
208
|
resolve(
|
|
@@ -253,7 +248,6 @@ class NostrBridge(
|
|
|
253
248
|
result
|
|
254
249
|
)
|
|
255
250
|
|
|
256
|
-
|
|
257
251
|
} catch (e: Exception) {
|
|
258
252
|
|
|
259
253
|
resolve(
|
|
@@ -289,7 +283,6 @@ class NostrBridge(
|
|
|
289
283
|
result
|
|
290
284
|
)
|
|
291
285
|
|
|
292
|
-
|
|
293
286
|
} catch (e: Exception) {
|
|
294
287
|
|
|
295
288
|
resolve(
|