Merge pull request #691 from greenart7c3/test_external_signer
Fixes/improvements to external signer
This commit is contained in:
@@ -26,9 +26,11 @@ import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
|||||||
import com.vitorpamplona.quartz.crypto.KeyPair
|
import com.vitorpamplona.quartz.crypto.KeyPair
|
||||||
import com.vitorpamplona.quartz.encoders.hexToByteArray
|
import com.vitorpamplona.quartz.encoders.hexToByteArray
|
||||||
import com.vitorpamplona.quartz.encoders.toHexKey
|
import com.vitorpamplona.quartz.encoders.toHexKey
|
||||||
|
import com.vitorpamplona.quartz.encoders.toNpub
|
||||||
import com.vitorpamplona.quartz.events.ContactListEvent
|
import com.vitorpamplona.quartz.events.ContactListEvent
|
||||||
import com.vitorpamplona.quartz.events.Event
|
import com.vitorpamplona.quartz.events.Event
|
||||||
import com.vitorpamplona.quartz.events.LnZapEvent
|
import com.vitorpamplona.quartz.events.LnZapEvent
|
||||||
|
import com.vitorpamplona.quartz.signers.ExternalSignerLauncher
|
||||||
import com.vitorpamplona.quartz.signers.NostrSignerExternal
|
import com.vitorpamplona.quartz.signers.NostrSignerExternal
|
||||||
import com.vitorpamplona.quartz.signers.NostrSignerInternal
|
import com.vitorpamplona.quartz.signers.NostrSignerInternal
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -89,6 +91,7 @@ private object PrefKeys {
|
|||||||
const val AUTOMATICALLY_HIDE_NAV_BARS = "automatically_hide_nav_bars"
|
const val AUTOMATICALLY_HIDE_NAV_BARS = "automatically_hide_nav_bars"
|
||||||
const val LOGIN_WITH_EXTERNAL_SIGNER = "login_with_external_signer"
|
const val LOGIN_WITH_EXTERNAL_SIGNER = "login_with_external_signer"
|
||||||
const val AUTOMATICALLY_SHOW_PROFILE_PICTURE = "automatically_show_profile_picture"
|
const val AUTOMATICALLY_SHOW_PROFILE_PICTURE = "automatically_show_profile_picture"
|
||||||
|
const val SIGNER_PACKAGE_NAME = "signer_package_name"
|
||||||
|
|
||||||
const val ALL_ACCOUNT_INFO = "all_saved_accounts_info"
|
const val ALL_ACCOUNT_INFO = "all_saved_accounts_info"
|
||||||
const val SHARED_SETTINGS = "shared_settings"
|
const val SHARED_SETTINGS = "shared_settings"
|
||||||
@@ -249,6 +252,7 @@ object LocalPreferences {
|
|||||||
putBoolean(PrefKeys.LOGIN_WITH_EXTERNAL_SIGNER, account.signer is NostrSignerExternal)
|
putBoolean(PrefKeys.LOGIN_WITH_EXTERNAL_SIGNER, account.signer is NostrSignerExternal)
|
||||||
if (account.signer is NostrSignerExternal) {
|
if (account.signer is NostrSignerExternal) {
|
||||||
remove(PrefKeys.NOSTR_PRIVKEY)
|
remove(PrefKeys.NOSTR_PRIVKEY)
|
||||||
|
putString(PrefKeys.SIGNER_PACKAGE_NAME, account.signer.launcher.signerPackageName)
|
||||||
} else {
|
} else {
|
||||||
account.keyPair.privKey?.let { putString(PrefKeys.NOSTR_PRIVKEY, it.toHexKey()) }
|
account.keyPair.privKey?.let { putString(PrefKeys.NOSTR_PRIVKEY, it.toHexKey()) }
|
||||||
}
|
}
|
||||||
@@ -475,7 +479,8 @@ object LocalPreferences {
|
|||||||
|
|
||||||
val keyPair = KeyPair(privKey = privKey?.hexToByteArray(), pubKey = pubKey.hexToByteArray())
|
val keyPair = KeyPair(privKey = privKey?.hexToByteArray(), pubKey = pubKey.hexToByteArray())
|
||||||
val signer = if (loginWithExternalSigner) {
|
val signer = if (loginWithExternalSigner) {
|
||||||
NostrSignerExternal(pubKey)
|
val packageName = getString(PrefKeys.SIGNER_PACKAGE_NAME, null) ?: "com.greenart7c3.nostrsigner"
|
||||||
|
NostrSignerExternal(pubKey, ExternalSignerLauncher(pubKey.hexToByteArray().toNpub(), packageName))
|
||||||
} else {
|
} else {
|
||||||
NostrSignerInternal(keyPair)
|
NostrSignerInternal(keyPair)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,6 +49,8 @@ import kotlinx.coroutines.GlobalScope
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.net.URLEncoder
|
import java.net.URLEncoder
|
||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
|
import java.util.Timer
|
||||||
|
import kotlin.concurrent.schedule
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity() {
|
class MainActivity : AppCompatActivity() {
|
||||||
private val isOnMobileDataState = mutableStateOf(false)
|
private val isOnMobileDataState = mutableStateOf(false)
|
||||||
@@ -108,6 +110,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
DefaultMutedSetting.value = true
|
DefaultMutedSetting.value = true
|
||||||
|
|
||||||
// Keep connection alive if it's calling the signer app
|
// Keep connection alive if it's calling the signer app
|
||||||
|
Log.d("shouldPauseService", "shouldPauseService onResume: $shouldPauseService")
|
||||||
if (shouldPauseService) {
|
if (shouldPauseService) {
|
||||||
GlobalScope.launch(Dispatchers.IO) {
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
serviceManager.justStart()
|
serviceManager.justStart()
|
||||||
@@ -121,7 +124,9 @@ class MainActivity : AppCompatActivity() {
|
|||||||
(getSystemService(ConnectivityManager::class.java) as ConnectivityManager).registerDefaultNetworkCallback(networkCallback)
|
(getSystemService(ConnectivityManager::class.java) as ConnectivityManager).registerDefaultNetworkCallback(networkCallback)
|
||||||
|
|
||||||
// resets state until next External Signer Call
|
// resets state until next External Signer Call
|
||||||
shouldPauseService = true
|
Timer().schedule(350) {
|
||||||
|
shouldPauseService = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
@@ -134,6 +139,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
Log.d("shouldPauseService", "shouldPauseService onPause: $shouldPauseService")
|
||||||
if (shouldPauseService) {
|
if (shouldPauseService) {
|
||||||
GlobalScope.launch(Dispatchers.IO) {
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
serviceManager.pauseForGood()
|
serviceManager.pauseForGood()
|
||||||
@@ -171,8 +177,11 @@ class MainActivity : AppCompatActivity() {
|
|||||||
override fun onAvailable(network: Network) {
|
override fun onAvailable(network: Network) {
|
||||||
super.onAvailable(network)
|
super.onAvailable(network)
|
||||||
|
|
||||||
GlobalScope.launch(Dispatchers.IO) {
|
Log.d("shouldPauseService", "shouldPauseService onAvailable: $shouldPauseService")
|
||||||
serviceManager.forceRestart()
|
if (shouldPauseService) {
|
||||||
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
|
serviceManager.forceRestart()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,7 +211,8 @@ class MainActivity : AppCompatActivity() {
|
|||||||
changedNetwork = true
|
changedNetwork = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (changedNetwork) {
|
Log.d("shouldPauseService", "shouldPauseService onCapabilitiesChanged: $shouldPauseService")
|
||||||
|
if (changedNetwork && shouldPauseService) {
|
||||||
GlobalScope.launch(Dispatchers.IO) {
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
serviceManager.forceRestart()
|
serviceManager.forceRestart()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.model.Account
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver
|
import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver
|
||||||
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
|
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
|
||||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||||
@@ -170,6 +171,7 @@ fun InvoiceRequest(
|
|||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
account.createZapRequestFor(toUserPubKeyHex, message, account.defaultZapType) { zapRequest ->
|
account.createZapRequestFor(toUserPubKeyHex, message, account.defaultZapType) { zapRequest ->
|
||||||
|
LocalCache.justConsume(zapRequest, null)
|
||||||
LightningAddressResolver().lnAddressInvoice(
|
LightningAddressResolver().lnAddressInvoice(
|
||||||
lud16,
|
lud16,
|
||||||
amount * 1000,
|
amount * 1000,
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ import com.vitorpamplona.quartz.encoders.Nip19
|
|||||||
import com.vitorpamplona.quartz.encoders.bechToBytes
|
import com.vitorpamplona.quartz.encoders.bechToBytes
|
||||||
import com.vitorpamplona.quartz.encoders.hexToByteArray
|
import com.vitorpamplona.quartz.encoders.hexToByteArray
|
||||||
import com.vitorpamplona.quartz.encoders.toHexKey
|
import com.vitorpamplona.quartz.encoders.toHexKey
|
||||||
|
import com.vitorpamplona.quartz.encoders.toNpub
|
||||||
|
import com.vitorpamplona.quartz.signers.ExternalSignerLauncher
|
||||||
import com.vitorpamplona.quartz.signers.NostrSignerExternal
|
import com.vitorpamplona.quartz.signers.NostrSignerExternal
|
||||||
import com.vitorpamplona.quartz.signers.NostrSignerInternal
|
import com.vitorpamplona.quartz.signers.NostrSignerInternal
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
@@ -56,7 +58,8 @@ class AccountStateViewModel() : ViewModel() {
|
|||||||
key: String,
|
key: String,
|
||||||
useProxy: Boolean,
|
useProxy: Boolean,
|
||||||
proxyPort: Int,
|
proxyPort: Int,
|
||||||
loginWithExternalSigner: Boolean = false
|
loginWithExternalSigner: Boolean = false,
|
||||||
|
packageName: String = ""
|
||||||
) = withContext(Dispatchers.IO) {
|
) = withContext(Dispatchers.IO) {
|
||||||
val parsed = Nip19.uriToRoute(key)
|
val parsed = Nip19.uriToRoute(key)
|
||||||
val pubKeyParsed = parsed?.hex?.hexToByteArray()
|
val pubKeyParsed = parsed?.hex?.hexToByteArray()
|
||||||
@@ -69,7 +72,8 @@ class AccountStateViewModel() : ViewModel() {
|
|||||||
val account =
|
val account =
|
||||||
if (loginWithExternalSigner) {
|
if (loginWithExternalSigner) {
|
||||||
val keyPair = KeyPair(pubKey = pubKeyParsed)
|
val keyPair = KeyPair(pubKey = pubKeyParsed)
|
||||||
Account(keyPair, proxy = proxy, proxyPort = proxyPort, signer = NostrSignerExternal(keyPair.pubKey.toHexKey()))
|
val localPackageName = packageName.ifBlank { "com.greenart7c3.nostrsigner" }
|
||||||
|
Account(keyPair, proxy = proxy, proxyPort = proxyPort, signer = NostrSignerExternal(keyPair.pubKey.toHexKey(), ExternalSignerLauncher(keyPair.pubKey.toNpub(), localPackageName)))
|
||||||
} else if (key.startsWith("nsec")) {
|
} else if (key.startsWith("nsec")) {
|
||||||
val keyPair = KeyPair(privKey = key.bechToBytes())
|
val keyPair = KeyPair(privKey = key.bechToBytes())
|
||||||
Account(keyPair, proxy = proxy, proxyPort = proxyPort, signer = NostrSignerInternal(keyPair))
|
Account(keyPair, proxy = proxy, proxyPort = proxyPort, signer = NostrSignerInternal(keyPair))
|
||||||
@@ -130,11 +134,12 @@ class AccountStateViewModel() : ViewModel() {
|
|||||||
useProxy: Boolean,
|
useProxy: Boolean,
|
||||||
proxyPort: Int,
|
proxyPort: Int,
|
||||||
loginWithExternalSigner: Boolean = false,
|
loginWithExternalSigner: Boolean = false,
|
||||||
|
packageName: String = "",
|
||||||
onError: () -> Unit
|
onError: () -> Unit
|
||||||
) {
|
) {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
loginAndStartUI(key, useProxy, proxyPort, loginWithExternalSigner)
|
loginAndStartUI(key, useProxy, proxyPort, loginWithExternalSigner, packageName)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("Login", "Could not sign in", e)
|
Log.e("Login", "Could not sign in", e)
|
||||||
onError()
|
onError()
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ fun LoginPage(
|
|||||||
var loginWithExternalSigner by remember { mutableStateOf(false) }
|
var loginWithExternalSigner by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
if (loginWithExternalSigner) {
|
if (loginWithExternalSigner) {
|
||||||
val externalSignerLauncher = remember { ExternalSignerLauncher("") }
|
val externalSignerLauncher = remember { ExternalSignerLauncher("", signerPackageName = "") }
|
||||||
val id = remember { UUID.randomUUID().toString() }
|
val id = remember { UUID.randomUUID().toString() }
|
||||||
|
|
||||||
val launcher = rememberLauncherForActivityResult(
|
val launcher = rememberLauncherForActivityResult(
|
||||||
@@ -162,8 +162,11 @@ fun LoginPage(
|
|||||||
SignerType.GET_PUBLIC_KEY,
|
SignerType.GET_PUBLIC_KEY,
|
||||||
"",
|
"",
|
||||||
id
|
id
|
||||||
) { pubkey ->
|
) { result ->
|
||||||
println("AAAA- COME BACK")
|
println("AAAA- COME BACK")
|
||||||
|
val split = result.split("-")
|
||||||
|
val pubkey = split.first()
|
||||||
|
val packageName = if (split.size > 1) split[1] else ""
|
||||||
key.value = TextFieldValue(pubkey)
|
key.value = TextFieldValue(pubkey)
|
||||||
if (!acceptedTerms.value) {
|
if (!acceptedTerms.value) {
|
||||||
termsAcceptanceIsRequired =
|
termsAcceptanceIsRequired =
|
||||||
@@ -175,7 +178,7 @@ fun LoginPage(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (acceptedTerms.value && key.value.text.isNotBlank()) {
|
if (acceptedTerms.value && key.value.text.isNotBlank()) {
|
||||||
accountViewModel.login(key.value.text, useProxy.value, proxyPort.value.toInt(), true) {
|
accountViewModel.login(key.value.text, useProxy.value, proxyPort.value.toInt(), true, packageName) {
|
||||||
errorMessage = context.getString(R.string.invalid_key)
|
errorMessage = context.getString(R.string.invalid_key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -432,33 +435,8 @@ fun LoginPage(
|
|||||||
return@Button
|
return@Button
|
||||||
}
|
}
|
||||||
|
|
||||||
val result = ExternalSignerLauncher("").getDataFromResolver(
|
loginWithExternalSigner = true
|
||||||
SignerType.GET_PUBLIC_KEY,
|
return@Button
|
||||||
arrayOf("login"),
|
|
||||||
contentResolver = Amethyst.instance.contentResolver
|
|
||||||
)
|
|
||||||
|
|
||||||
if (result == null) {
|
|
||||||
loginWithExternalSigner = true
|
|
||||||
return@Button
|
|
||||||
} else {
|
|
||||||
key.value = TextFieldValue(result)
|
|
||||||
if (key.value.text.isBlank()) {
|
|
||||||
errorMessage = context.getString(R.string.key_is_required)
|
|
||||||
return@Button
|
|
||||||
}
|
|
||||||
|
|
||||||
if (acceptedTerms.value && key.value.text.isNotBlank()) {
|
|
||||||
accountViewModel.login(
|
|
||||||
key.value.text,
|
|
||||||
useProxy.value,
|
|
||||||
proxyPort.value.toInt(),
|
|
||||||
true
|
|
||||||
) {
|
|
||||||
errorMessage = context.getString(R.string.invalid_key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
shape = RoundedCornerShape(Size35dp),
|
shape = RoundedCornerShape(Size35dp),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|||||||
@@ -19,9 +19,18 @@ enum class SignerType {
|
|||||||
DECRYPT_ZAP_EVENT
|
DECRYPT_ZAP_EVENT
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class Permission(
|
||||||
|
val type: String,
|
||||||
|
val kind: Int? = null
|
||||||
|
) {
|
||||||
|
fun toJson(): String {
|
||||||
|
return "{\"type\":\"${type}\",\"kind\":${kind}}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class ExternalSignerLauncher(
|
class ExternalSignerLauncher(
|
||||||
private val npub: String,
|
private val npub: String,
|
||||||
private val signerPackageName: String = "com.greenart7c3.nostrsigner"
|
val signerPackageName: String = "com.greenart7c3.nostrsigner"
|
||||||
) {
|
) {
|
||||||
private val contentCache = LruCache<String, (String) -> Unit>(20)
|
private val contentCache = LruCache<String, (String) -> Unit>(20)
|
||||||
|
|
||||||
@@ -49,9 +58,11 @@ class ExternalSignerLauncher(
|
|||||||
|
|
||||||
fun newResult(data: Intent) {
|
fun newResult(data: Intent) {
|
||||||
val signature = data.getStringExtra("signature") ?: ""
|
val signature = data.getStringExtra("signature") ?: ""
|
||||||
|
val packageName = data.getStringExtra("package") ?: ""
|
||||||
val id = data.getStringExtra("id") ?: ""
|
val id = data.getStringExtra("id") ?: ""
|
||||||
if (id.isNotBlank()) {
|
if (id.isNotBlank()) {
|
||||||
contentCache.get(id)?.invoke(signature)
|
val result = if (packageName.isNotBlank()) "$signature-$packageName" else signature
|
||||||
|
contentCache.get(id)?.invoke(result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,6 +81,40 @@ class ExternalSignerLauncher(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun defaultPermissions(): String {
|
||||||
|
val permissions = listOf(
|
||||||
|
Permission(
|
||||||
|
"sign_event",
|
||||||
|
22242
|
||||||
|
),
|
||||||
|
Permission(
|
||||||
|
"nip04_encrypt"
|
||||||
|
),
|
||||||
|
Permission(
|
||||||
|
"nip04_decrypt"
|
||||||
|
),
|
||||||
|
Permission(
|
||||||
|
"nip44_encrypt"
|
||||||
|
),
|
||||||
|
Permission(
|
||||||
|
"nip44_decrypt"
|
||||||
|
),
|
||||||
|
Permission(
|
||||||
|
"decrypt_zap_event"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
val jsonArray = StringBuilder("[")
|
||||||
|
permissions.forEachIndexed { index, permission ->
|
||||||
|
jsonArray.append(permission.toJson())
|
||||||
|
if (index < permissions.size - 1) {
|
||||||
|
jsonArray.append(",")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
jsonArray.append("]")
|
||||||
|
|
||||||
|
return jsonArray.toString()
|
||||||
|
}
|
||||||
|
|
||||||
private fun openSignerApp(
|
private fun openSignerApp(
|
||||||
data: String,
|
data: String,
|
||||||
type: SignerType,
|
type: SignerType,
|
||||||
@@ -93,8 +138,12 @@ class ExternalSignerLauncher(
|
|||||||
intent.putExtra("id", id)
|
intent.putExtra("id", id)
|
||||||
if (type !== SignerType.GET_PUBLIC_KEY) {
|
if (type !== SignerType.GET_PUBLIC_KEY) {
|
||||||
intent.putExtra("current_user", npub)
|
intent.putExtra("current_user", npub)
|
||||||
|
} else {
|
||||||
|
intent.putExtra("permissions", defaultPermissions())
|
||||||
|
}
|
||||||
|
if (signerPackageName.isNotBlank()) {
|
||||||
|
intent.`package` = signerPackageName
|
||||||
}
|
}
|
||||||
intent.`package` = signerPackageName
|
|
||||||
|
|
||||||
contentCache.put(id, onReady)
|
contentCache.put(id, onReady)
|
||||||
|
|
||||||
|
|||||||
@@ -34,17 +34,33 @@ class NostrSignerExternal(
|
|||||||
)
|
)
|
||||||
|
|
||||||
launcher.openSigner(event) { signature ->
|
launcher.openSigner(event) { signature ->
|
||||||
(EventFactory.create(
|
if (signature.startsWith("{")) {
|
||||||
event.id,
|
val localEvent = Event.fromJson(signature)
|
||||||
event.pubKey,
|
(EventFactory.create(
|
||||||
event.createdAt,
|
localEvent.id,
|
||||||
event.kind,
|
localEvent.pubKey,
|
||||||
event.tags,
|
localEvent.createdAt,
|
||||||
event.content,
|
localEvent.kind,
|
||||||
signature
|
localEvent.tags,
|
||||||
) as? T?)?.let {
|
localEvent.content,
|
||||||
onReady(it)
|
localEvent.sig
|
||||||
|
) as? T?)?.let {
|
||||||
|
onReady(it)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
(EventFactory.create(
|
||||||
|
event.id,
|
||||||
|
event.pubKey,
|
||||||
|
event.createdAt,
|
||||||
|
event.kind,
|
||||||
|
event.tags,
|
||||||
|
event.content,
|
||||||
|
signature
|
||||||
|
) as? T?)?.let {
|
||||||
|
onReady(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user