Merge pull request #1954 from vitorpamplona/claude/nip86-relay-management-KuTru
Add NIP-86 relay management UI and client implementation
This commit is contained in:
+88
@@ -0,0 +1,88 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.model.nip86RelayManagement
|
||||||
|
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||||
|
import com.vitorpamplona.quartz.nip86RelayManagement.Nip86Client
|
||||||
|
import com.vitorpamplona.quartz.nip86RelayManagement.rpc.Nip86Request
|
||||||
|
import com.vitorpamplona.quartz.nip86RelayManagement.rpc.Nip86Response
|
||||||
|
import com.vitorpamplona.quartz.utils.Log
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
|
import okhttp3.coroutines.executeAsync
|
||||||
|
|
||||||
|
class Nip86Retriever(
|
||||||
|
val okHttpClient: (NormalizedRelayUrl) -> OkHttpClient,
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
val CONTENT_TYPE = "application/nostr+json+rpc".toMediaType()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun execute(
|
||||||
|
client: Nip86Client,
|
||||||
|
request: Nip86Request,
|
||||||
|
): Nip86Response {
|
||||||
|
val jsonBody = client.serializeRequest(request)
|
||||||
|
val bodyBytes = jsonBody.encodeToByteArray()
|
||||||
|
val authToken = client.buildAuthHeader(bodyBytes)
|
||||||
|
|
||||||
|
val httpRequest =
|
||||||
|
Request
|
||||||
|
.Builder()
|
||||||
|
.url(client.httpUrl)
|
||||||
|
.header("Content-Type", "application/nostr+json+rpc")
|
||||||
|
.header("Authorization", authToken)
|
||||||
|
.post(bodyBytes.toRequestBody(CONTENT_TYPE))
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val httpClient = okHttpClient(client.relayUrl)
|
||||||
|
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
httpClient.newCall(httpRequest).executeAsync().use { response ->
|
||||||
|
val body = response.body.string()
|
||||||
|
if (response.code == 401) {
|
||||||
|
Nip86Response(error = "Unauthorized: relay rejected authentication")
|
||||||
|
} else if (!response.isSuccessful) {
|
||||||
|
Nip86Response(error = "HTTP ${response.code}: $body")
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
client.parseResponse(body)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (e is CancellationException) throw e
|
||||||
|
Log.e("Nip86Retriever", "Failed to parse response: $body", e)
|
||||||
|
Nip86Response(error = "Failed to parse response: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (e is CancellationException) throw e
|
||||||
|
Log.e("Nip86Retriever", "Failed to reach relay ${client.relayUrl.url}", e)
|
||||||
|
Nip86Response(error = "Failed to reach relay: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -108,6 +108,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.relay.RelayFeedScreen
|
|||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.AllRelayListScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.AllRelayListScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.RelayInformationScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.RelayInformationScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.eventsync.EventSyncScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.eventsync.EventSyncScreen
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.nip86.RelayManagementScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.search.SearchScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.search.SearchScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.AllSettingsScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.AllSettingsScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.NIP47SetupScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.NIP47SetupScreen
|
||||||
@@ -228,6 +229,7 @@ fun BuildNavigation(
|
|||||||
composableFromEndArgs<Route.RelayFeed> { RelayFeedScreen(it, accountViewModel, nav) }
|
composableFromEndArgs<Route.RelayFeed> { RelayFeedScreen(it, accountViewModel, nav) }
|
||||||
composableFromEndArgs<Route.ChessGame> { ChessGameScreen(it.gameId, accountViewModel, nav) }
|
composableFromEndArgs<Route.ChessGame> { ChessGameScreen(it.gameId, accountViewModel, nav) }
|
||||||
composableFromEndArgs<Route.RelayInfo> { RelayInformationScreen(it.url, accountViewModel, nav) }
|
composableFromEndArgs<Route.RelayInfo> { RelayInformationScreen(it.url, accountViewModel, nav) }
|
||||||
|
composableFromEndArgs<Route.RelayManagement> { RelayManagementScreen(it.url, accountViewModel, nav) }
|
||||||
composableFromEndArgs<Route.Community> { CommunityScreen(Address(it.kind, it.pubKeyHex, it.dTag), accountViewModel, nav) }
|
composableFromEndArgs<Route.Community> { CommunityScreen(Address(it.kind, it.pubKeyHex, it.dTag), accountViewModel, nav) }
|
||||||
composableFromEndArgs<Route.FollowPack> { FollowPackFeedScreen(Address(it.kind, it.pubKeyHex, it.dTag), accountViewModel, nav) }
|
composableFromEndArgs<Route.FollowPack> { FollowPackFeedScreen(Address(it.kind, it.pubKeyHex, it.dTag), accountViewModel, nav) }
|
||||||
|
|
||||||
|
|||||||
@@ -220,6 +220,10 @@ sealed class Route {
|
|||||||
val url: String,
|
val url: String,
|
||||||
) : Route()
|
) : Route()
|
||||||
|
|
||||||
|
@Serializable data class RelayManagement(
|
||||||
|
val url: String,
|
||||||
|
) : Route()
|
||||||
|
|
||||||
@Serializable data class RelayFeed(
|
@Serializable data class RelayFeed(
|
||||||
val url: String,
|
val url: String,
|
||||||
) : Route()
|
) : Route()
|
||||||
|
|||||||
+19
-1
@@ -62,6 +62,7 @@ import androidx.compose.material.icons.filled.Language
|
|||||||
import androidx.compose.material.icons.filled.Lock
|
import androidx.compose.material.icons.filled.Lock
|
||||||
import androidx.compose.material.icons.filled.Payment
|
import androidx.compose.material.icons.filled.Payment
|
||||||
import androidx.compose.material.icons.filled.PrivacyTip
|
import androidx.compose.material.icons.filled.PrivacyTip
|
||||||
|
import androidx.compose.material.icons.filled.Shield
|
||||||
import androidx.compose.material.icons.filled.Storage
|
import androidx.compose.material.icons.filled.Storage
|
||||||
import androidx.compose.material.icons.filled.Tag
|
import androidx.compose.material.icons.filled.Tag
|
||||||
import androidx.compose.material.icons.filled.Topic
|
import androidx.compose.material.icons.filled.Topic
|
||||||
@@ -254,6 +255,7 @@ import com.vitorpamplona.quartz.nip72ModCommunities.follow.CommunityListEvent
|
|||||||
import com.vitorpamplona.quartz.nip75ZapGoals.GoalEvent
|
import com.vitorpamplona.quartz.nip75ZapGoals.GoalEvent
|
||||||
import com.vitorpamplona.quartz.nip78AppData.AppSpecificDataEvent
|
import com.vitorpamplona.quartz.nip78AppData.AppSpecificDataEvent
|
||||||
import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent
|
import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent
|
||||||
|
import com.vitorpamplona.quartz.nip86RelayManagement.Nip86Client
|
||||||
import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent
|
import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent
|
||||||
import com.vitorpamplona.quartz.nip88Polls.response.PollResponseEvent
|
import com.vitorpamplona.quartz.nip88Polls.response.PollResponseEvent
|
||||||
import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent
|
import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent
|
||||||
@@ -307,7 +309,23 @@ fun RelayInformationScreen(
|
|||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
actions = {},
|
actions = {
|
||||||
|
val relayInfo by loadRelayInfo(relay)
|
||||||
|
if (Nip86Client.supportsNip86(relayInfo.supported_nips)) {
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = { nav.nav(Route.RelayManagement(relay.url)) },
|
||||||
|
shape = ButtonBorder,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Shield,
|
||||||
|
contentDescription = stringRes(R.string.relay_management_button),
|
||||||
|
modifier = Height25Modifier,
|
||||||
|
)
|
||||||
|
Spacer(modifier = StdHorzSpacer)
|
||||||
|
Text(stringRes(R.string.relay_management_button))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
title = {
|
title = {
|
||||||
Text(
|
Text(
|
||||||
relay.displayUrl(),
|
relay.displayUrl(),
|
||||||
|
|||||||
+953
@@ -0,0 +1,953 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.nip86
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Add
|
||||||
|
import androidx.compose.material.icons.filled.Block
|
||||||
|
import androidx.compose.material.icons.filled.CheckCircle
|
||||||
|
import androidx.compose.material.icons.filled.Close
|
||||||
|
import androidx.compose.material.icons.filled.Delete
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.ScrollableTabRow
|
||||||
|
import androidx.compose.material3.Snackbar
|
||||||
|
import androidx.compose.material3.Tab
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.material3.TopAppBar
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
|
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.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.vitorpamplona.amethyst.Amethyst
|
||||||
|
import com.vitorpamplona.amethyst.R
|
||||||
|
import com.vitorpamplona.amethyst.model.nip86RelayManagement.Nip86Retriever
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.BackButton
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl
|
||||||
|
import com.vitorpamplona.quartz.nip86RelayManagement.rpc.Nip86Method
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RelayManagementScreen(
|
||||||
|
relayUrl: String,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
nav: INav,
|
||||||
|
) {
|
||||||
|
RelayUrlNormalizer.normalizeOrNull(relayUrl)?.let {
|
||||||
|
RelayManagementScreen(
|
||||||
|
relay = it,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
nav = nav,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun RelayManagementScreen(
|
||||||
|
relay: NormalizedRelayUrl,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
nav: INav,
|
||||||
|
) {
|
||||||
|
val retriever =
|
||||||
|
remember {
|
||||||
|
Nip86Retriever(Amethyst.instance.torEvaluatorFlow::okHttpClientForRelay)
|
||||||
|
}
|
||||||
|
|
||||||
|
val viewModel =
|
||||||
|
remember(relay) {
|
||||||
|
RelayManagementViewModel(
|
||||||
|
relayUrl = relay,
|
||||||
|
signer = accountViewModel.account.signer,
|
||||||
|
retriever = retriever,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(relay) {
|
||||||
|
viewModel.loadSupportedMethods()
|
||||||
|
}
|
||||||
|
|
||||||
|
val supportedMethods by viewModel.supportedMethods.collectAsState()
|
||||||
|
val isLoading by viewModel.isLoading.collectAsState()
|
||||||
|
val error by viewModel.error.collectAsState()
|
||||||
|
|
||||||
|
LaunchedEffect(supportedMethods) {
|
||||||
|
if (supportedMethods.isNotEmpty()) {
|
||||||
|
viewModel.loadAllLists()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
actions = {},
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.relay_management_title, relay.displayUrl()),
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
navigationIcon = {
|
||||||
|
Row {
|
||||||
|
Spacer(modifier = StdHorzSpacer)
|
||||||
|
BackButton(onPress = nav::popBack)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
) { pad ->
|
||||||
|
if (isLoading && supportedMethods.isEmpty()) {
|
||||||
|
Column(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.padding(pad)
|
||||||
|
.consumeWindowInsets(pad)
|
||||||
|
.fillMaxSize(),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
) {
|
||||||
|
CircularProgressIndicator()
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
Text(stringResource(R.string.relay_management_loading))
|
||||||
|
}
|
||||||
|
} else if (supportedMethods.isEmpty() && error != null) {
|
||||||
|
Column(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.padding(pad)
|
||||||
|
.consumeWindowInsets(pad)
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(16.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Block,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(48.dp),
|
||||||
|
tint = MaterialTheme.colorScheme.error,
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.relay_management_error),
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Text(
|
||||||
|
error ?: "",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.error,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
RelayManagementContent(pad, viewModel, supportedMethods, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun RelayManagementContent(
|
||||||
|
pad: PaddingValues,
|
||||||
|
viewModel: RelayManagementViewModel,
|
||||||
|
supportedMethods: List<String>,
|
||||||
|
error: String?,
|
||||||
|
) {
|
||||||
|
val tabs =
|
||||||
|
remember(supportedMethods) {
|
||||||
|
buildList {
|
||||||
|
if (supportedMethods.any { it in listOf(Nip86Method.BAN_PUBKEY, Nip86Method.LIST_BANNED_PUBKEYS, Nip86Method.ALLOW_PUBKEY, Nip86Method.LIST_ALLOWED_PUBKEYS) }) {
|
||||||
|
add(ManagementTab.PUBKEYS)
|
||||||
|
}
|
||||||
|
if (supportedMethods.any {
|
||||||
|
it in listOf(Nip86Method.BAN_EVENT, Nip86Method.LIST_BANNED_EVENTS, Nip86Method.ALLOW_EVENT, Nip86Method.LIST_EVENTS_NEEDING_MODERATION)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
add(ManagementTab.EVENTS)
|
||||||
|
}
|
||||||
|
if (supportedMethods.any { it in listOf(Nip86Method.ALLOW_KIND, Nip86Method.DISALLOW_KIND, Nip86Method.LIST_ALLOWED_KINDS) }) {
|
||||||
|
add(ManagementTab.KINDS)
|
||||||
|
}
|
||||||
|
if (supportedMethods.any { it in listOf(Nip86Method.BLOCK_IP, Nip86Method.UNBLOCK_IP, Nip86Method.LIST_BLOCKED_IPS) }) {
|
||||||
|
add(ManagementTab.IPS)
|
||||||
|
}
|
||||||
|
if (supportedMethods.any { it in listOf(Nip86Method.CHANGE_RELAY_NAME, Nip86Method.CHANGE_RELAY_DESCRIPTION, Nip86Method.CHANGE_RELAY_ICON) }) {
|
||||||
|
add(ManagementTab.SETTINGS)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var selectedTab by remember { mutableIntStateOf(0) }
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.padding(pad)
|
||||||
|
.consumeWindowInsets(pad)
|
||||||
|
.fillMaxSize(),
|
||||||
|
) {
|
||||||
|
if (error != null) {
|
||||||
|
Snackbar(
|
||||||
|
modifier = Modifier.padding(8.dp),
|
||||||
|
action = {
|
||||||
|
TextButton(onClick = { viewModel.clearError() }) {
|
||||||
|
Text(stringResource(R.string.relay_management_dismiss))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Text(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tabs.isNotEmpty()) {
|
||||||
|
ScrollableTabRow(
|
||||||
|
selectedTabIndex = selectedTab.coerceAtMost(tabs.size - 1),
|
||||||
|
edgePadding = 8.dp,
|
||||||
|
) {
|
||||||
|
tabs.forEachIndexed { index, tab ->
|
||||||
|
Tab(
|
||||||
|
selected = selectedTab == index,
|
||||||
|
onClick = { selectedTab = index },
|
||||||
|
text = { Text(stringResource(tab.titleRes)) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
when (tabs.getOrNull(selectedTab)) {
|
||||||
|
ManagementTab.PUBKEYS -> {
|
||||||
|
PubkeysTab(viewModel, supportedMethods)
|
||||||
|
}
|
||||||
|
|
||||||
|
ManagementTab.EVENTS -> {
|
||||||
|
EventsTab(viewModel, supportedMethods)
|
||||||
|
}
|
||||||
|
|
||||||
|
ManagementTab.KINDS -> {
|
||||||
|
KindsTab(viewModel, supportedMethods)
|
||||||
|
}
|
||||||
|
|
||||||
|
ManagementTab.IPS -> {
|
||||||
|
IpsTab(viewModel, supportedMethods)
|
||||||
|
}
|
||||||
|
|
||||||
|
ManagementTab.SETTINGS -> {
|
||||||
|
SettingsTab(viewModel, supportedMethods)
|
||||||
|
}
|
||||||
|
|
||||||
|
null -> {}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxSize().padding(16.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.relay_management_no_methods),
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum class ManagementTab(
|
||||||
|
val titleRes: Int,
|
||||||
|
) {
|
||||||
|
PUBKEYS(R.string.relay_management_tab_pubkeys),
|
||||||
|
EVENTS(R.string.relay_management_tab_events),
|
||||||
|
KINDS(R.string.relay_management_tab_kinds),
|
||||||
|
IPS(R.string.relay_management_tab_ips),
|
||||||
|
SETTINGS(R.string.relay_management_tab_settings),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pubkeys Tab
|
||||||
|
@Composable
|
||||||
|
private fun PubkeysTab(
|
||||||
|
viewModel: RelayManagementViewModel,
|
||||||
|
supportedMethods: List<String>,
|
||||||
|
) {
|
||||||
|
val bannedPubkeys by viewModel.bannedPubkeys.collectAsState()
|
||||||
|
val allowedPubkeys by viewModel.allowedPubkeys.collectAsState()
|
||||||
|
var showBanDialog by remember { mutableStateOf(false) }
|
||||||
|
var showAllowDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
LazyColumn(
|
||||||
|
contentPadding = PaddingValues(10.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
if (supportedMethods.contains(Nip86Method.LIST_BANNED_PUBKEYS)) {
|
||||||
|
item {
|
||||||
|
SectionHeaderWithAdd(
|
||||||
|
stringResource(R.string.relay_management_banned_pubkeys),
|
||||||
|
showAdd = supportedMethods.contains(Nip86Method.BAN_PUBKEY),
|
||||||
|
onAdd = { showBanDialog = true },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bannedPubkeys.isEmpty()) {
|
||||||
|
item { EmptyListMessage(stringResource(R.string.relay_management_no_banned_pubkeys)) }
|
||||||
|
} else {
|
||||||
|
items(bannedPubkeys, key = { it.pubkey }) { entry ->
|
||||||
|
HexEntryCard(
|
||||||
|
hex = entry.pubkey,
|
||||||
|
reason = entry.reason,
|
||||||
|
showRemove = supportedMethods.contains(Nip86Method.UNBAN_PUBKEY),
|
||||||
|
onRemove = { viewModel.unbanPubkey(entry.pubkey) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (supportedMethods.contains(Nip86Method.LIST_ALLOWED_PUBKEYS)) {
|
||||||
|
item { Spacer(modifier = Modifier.height(8.dp)) }
|
||||||
|
item {
|
||||||
|
SectionHeaderWithAdd(
|
||||||
|
stringResource(R.string.relay_management_allowed_pubkeys),
|
||||||
|
showAdd = supportedMethods.contains(Nip86Method.ALLOW_PUBKEY),
|
||||||
|
onAdd = { showAllowDialog = true },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allowedPubkeys.isEmpty()) {
|
||||||
|
item { EmptyListMessage(stringResource(R.string.relay_management_no_allowed_pubkeys)) }
|
||||||
|
} else {
|
||||||
|
items(allowedPubkeys, key = { it.pubkey }) { entry ->
|
||||||
|
HexEntryCard(
|
||||||
|
hex = entry.pubkey,
|
||||||
|
reason = entry.reason,
|
||||||
|
showRemove = supportedMethods.contains(Nip86Method.UNALLOW_PUBKEY),
|
||||||
|
onRemove = { viewModel.unallowPubkey(entry.pubkey) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showBanDialog) {
|
||||||
|
HexInputDialog(
|
||||||
|
title = stringResource(R.string.relay_management_ban_pubkey),
|
||||||
|
label = stringResource(R.string.relay_management_pubkey_hex),
|
||||||
|
onConfirm = { hex, reason ->
|
||||||
|
viewModel.banPubkey(hex, reason.ifBlank { null })
|
||||||
|
showBanDialog = false
|
||||||
|
},
|
||||||
|
onDismiss = { showBanDialog = false },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showAllowDialog) {
|
||||||
|
HexInputDialog(
|
||||||
|
title = stringResource(R.string.relay_management_allow_pubkey),
|
||||||
|
label = stringResource(R.string.relay_management_pubkey_hex),
|
||||||
|
onConfirm = { hex, reason ->
|
||||||
|
viewModel.allowPubkey(hex, reason.ifBlank { null })
|
||||||
|
showAllowDialog = false
|
||||||
|
},
|
||||||
|
onDismiss = { showAllowDialog = false },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Events Tab
|
||||||
|
@Composable
|
||||||
|
private fun EventsTab(
|
||||||
|
viewModel: RelayManagementViewModel,
|
||||||
|
supportedMethods: List<String>,
|
||||||
|
) {
|
||||||
|
val bannedEvents by viewModel.bannedEvents.collectAsState()
|
||||||
|
val eventsNeedingModeration by viewModel.eventsNeedingModeration.collectAsState()
|
||||||
|
var showBanDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
LazyColumn(
|
||||||
|
contentPadding = PaddingValues(10.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
if (supportedMethods.contains(Nip86Method.LIST_EVENTS_NEEDING_MODERATION)) {
|
||||||
|
item {
|
||||||
|
SectionHeaderWithAdd(
|
||||||
|
stringResource(R.string.relay_management_moderation_queue),
|
||||||
|
showAdd = false,
|
||||||
|
onAdd = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventsNeedingModeration.isEmpty()) {
|
||||||
|
item { EmptyListMessage(stringResource(R.string.relay_management_no_moderation_events)) }
|
||||||
|
} else {
|
||||||
|
items(eventsNeedingModeration, key = { it.id }) { entry ->
|
||||||
|
ModerationEventCard(
|
||||||
|
eventId = entry.id,
|
||||||
|
reason = entry.reason,
|
||||||
|
canAllow = supportedMethods.contains(Nip86Method.ALLOW_EVENT),
|
||||||
|
canBan = supportedMethods.contains(Nip86Method.BAN_EVENT),
|
||||||
|
onAllow = { viewModel.allowEvent(entry.id) },
|
||||||
|
onBan = { viewModel.banEvent(entry.id) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (supportedMethods.contains(Nip86Method.LIST_BANNED_EVENTS)) {
|
||||||
|
item { Spacer(modifier = Modifier.height(8.dp)) }
|
||||||
|
item {
|
||||||
|
SectionHeaderWithAdd(
|
||||||
|
stringResource(R.string.relay_management_banned_events),
|
||||||
|
showAdd = supportedMethods.contains(Nip86Method.BAN_EVENT),
|
||||||
|
onAdd = { showBanDialog = true },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bannedEvents.isEmpty()) {
|
||||||
|
item { EmptyListMessage(stringResource(R.string.relay_management_no_banned_events)) }
|
||||||
|
} else {
|
||||||
|
items(bannedEvents, key = { it.id }) { entry ->
|
||||||
|
HexEntryCard(
|
||||||
|
hex = entry.id,
|
||||||
|
reason = entry.reason,
|
||||||
|
showRemove = false,
|
||||||
|
onRemove = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showBanDialog) {
|
||||||
|
HexInputDialog(
|
||||||
|
title = stringResource(R.string.relay_management_ban_event),
|
||||||
|
label = stringResource(R.string.relay_management_event_id_hex),
|
||||||
|
onConfirm = { hex, reason ->
|
||||||
|
viewModel.banEvent(hex, reason.ifBlank { null })
|
||||||
|
showBanDialog = false
|
||||||
|
},
|
||||||
|
onDismiss = { showBanDialog = false },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kinds Tab
|
||||||
|
@Composable
|
||||||
|
private fun KindsTab(
|
||||||
|
viewModel: RelayManagementViewModel,
|
||||||
|
supportedMethods: List<String>,
|
||||||
|
) {
|
||||||
|
val allowedKinds by viewModel.allowedKinds.collectAsState()
|
||||||
|
var showAddDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
LazyColumn(
|
||||||
|
contentPadding = PaddingValues(10.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
item {
|
||||||
|
SectionHeaderWithAdd(
|
||||||
|
stringResource(R.string.relay_management_allowed_kinds),
|
||||||
|
showAdd = supportedMethods.contains(Nip86Method.ALLOW_KIND),
|
||||||
|
onAdd = { showAddDialog = true },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allowedKinds.isEmpty()) {
|
||||||
|
item { EmptyListMessage(stringResource(R.string.relay_management_no_allowed_kinds)) }
|
||||||
|
} else {
|
||||||
|
items(allowedKinds, key = { it }) { kind ->
|
||||||
|
KindEntryCard(
|
||||||
|
kind = kind,
|
||||||
|
showRemove = supportedMethods.contains(Nip86Method.DISALLOW_KIND),
|
||||||
|
onRemove = { viewModel.disallowKind(kind) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showAddDialog) {
|
||||||
|
KindInputDialog(
|
||||||
|
onConfirm = { kind ->
|
||||||
|
viewModel.allowKind(kind)
|
||||||
|
showAddDialog = false
|
||||||
|
},
|
||||||
|
onDismiss = { showAddDialog = false },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IPs Tab
|
||||||
|
@Composable
|
||||||
|
private fun IpsTab(
|
||||||
|
viewModel: RelayManagementViewModel,
|
||||||
|
supportedMethods: List<String>,
|
||||||
|
) {
|
||||||
|
val blockedIps by viewModel.blockedIps.collectAsState()
|
||||||
|
var showBlockDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
LazyColumn(
|
||||||
|
contentPadding = PaddingValues(10.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
item {
|
||||||
|
SectionHeaderWithAdd(
|
||||||
|
stringResource(R.string.relay_management_blocked_ips),
|
||||||
|
showAdd = supportedMethods.contains(Nip86Method.BLOCK_IP),
|
||||||
|
onAdd = { showBlockDialog = true },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (blockedIps.isEmpty()) {
|
||||||
|
item { EmptyListMessage(stringResource(R.string.relay_management_no_blocked_ips)) }
|
||||||
|
} else {
|
||||||
|
items(blockedIps, key = { it.ip }) { entry ->
|
||||||
|
IpEntryCard(
|
||||||
|
ip = entry.ip,
|
||||||
|
reason = entry.reason,
|
||||||
|
showRemove = supportedMethods.contains(Nip86Method.UNBLOCK_IP),
|
||||||
|
onRemove = { viewModel.unblockIp(entry.ip) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showBlockDialog) {
|
||||||
|
HexInputDialog(
|
||||||
|
title = stringResource(R.string.relay_management_block_ip),
|
||||||
|
label = stringResource(R.string.relay_management_ip_address),
|
||||||
|
onConfirm = { ip, reason ->
|
||||||
|
viewModel.blockIp(ip, reason.ifBlank { null })
|
||||||
|
showBlockDialog = false
|
||||||
|
},
|
||||||
|
onDismiss = { showBlockDialog = false },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Settings Tab
|
||||||
|
@Composable
|
||||||
|
private fun SettingsTab(
|
||||||
|
viewModel: RelayManagementViewModel,
|
||||||
|
supportedMethods: List<String>,
|
||||||
|
) {
|
||||||
|
var relayName by remember { mutableStateOf("") }
|
||||||
|
var relayDescription by remember { mutableStateOf("") }
|
||||||
|
var relayIcon by remember { mutableStateOf("") }
|
||||||
|
|
||||||
|
LazyColumn(
|
||||||
|
contentPadding = PaddingValues(10.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
) {
|
||||||
|
if (supportedMethods.contains(Nip86Method.CHANGE_RELAY_NAME)) {
|
||||||
|
item {
|
||||||
|
SettingsField(
|
||||||
|
label = stringResource(R.string.relay_management_relay_name),
|
||||||
|
value = relayName,
|
||||||
|
onValueChange = { relayName = it },
|
||||||
|
onApply = { viewModel.changeRelayName(relayName) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (supportedMethods.contains(Nip86Method.CHANGE_RELAY_DESCRIPTION)) {
|
||||||
|
item {
|
||||||
|
SettingsField(
|
||||||
|
label = stringResource(R.string.relay_management_relay_description),
|
||||||
|
value = relayDescription,
|
||||||
|
onValueChange = { relayDescription = it },
|
||||||
|
onApply = { viewModel.changeRelayDescription(relayDescription) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (supportedMethods.contains(Nip86Method.CHANGE_RELAY_ICON)) {
|
||||||
|
item {
|
||||||
|
SettingsField(
|
||||||
|
label = stringResource(R.string.relay_management_relay_icon_url),
|
||||||
|
value = relayIcon,
|
||||||
|
onValueChange = { relayIcon = it },
|
||||||
|
onApply = { viewModel.changeRelayIcon(relayIcon) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reusable components
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SectionHeaderWithAdd(
|
||||||
|
title: String,
|
||||||
|
showAdd: Boolean,
|
||||||
|
onAdd: () -> Unit,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
)
|
||||||
|
if (showAdd) {
|
||||||
|
IconButton(onClick = onAdd) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Add,
|
||||||
|
contentDescription = stringResource(R.string.relay_management_add),
|
||||||
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
HorizontalDivider()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun EmptyListMessage(message: String) {
|
||||||
|
Text(
|
||||||
|
message,
|
||||||
|
modifier = Modifier.padding(vertical = 8.dp),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun HexEntryCard(
|
||||||
|
hex: String,
|
||||||
|
reason: String?,
|
||||||
|
showRemove: Boolean,
|
||||||
|
onRemove: () -> Unit,
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant),
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.padding(12.dp).fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
Text(
|
||||||
|
hex,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
fontFamily = FontFamily.Monospace,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
reason?.let {
|
||||||
|
Text(
|
||||||
|
it,
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (showRemove) {
|
||||||
|
IconButton(onClick = onRemove) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Close,
|
||||||
|
contentDescription = stringResource(R.string.relay_management_remove),
|
||||||
|
tint = MaterialTheme.colorScheme.error,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ModerationEventCard(
|
||||||
|
eventId: String,
|
||||||
|
reason: String?,
|
||||||
|
canAllow: Boolean,
|
||||||
|
canBan: Boolean,
|
||||||
|
onAllow: () -> Unit,
|
||||||
|
onBan: () -> Unit,
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant),
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.padding(12.dp)) {
|
||||||
|
Text(
|
||||||
|
eventId,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
fontFamily = FontFamily.Monospace,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
reason?.let {
|
||||||
|
Text(
|
||||||
|
it,
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.End,
|
||||||
|
) {
|
||||||
|
if (canAllow) {
|
||||||
|
IconButton(onClick = onAllow) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.CheckCircle,
|
||||||
|
contentDescription = stringResource(R.string.relay_management_allow),
|
||||||
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (canBan) {
|
||||||
|
IconButton(onClick = onBan) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Block,
|
||||||
|
contentDescription = stringResource(R.string.relay_management_ban),
|
||||||
|
tint = MaterialTheme.colorScheme.error,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun KindEntryCard(
|
||||||
|
kind: Int,
|
||||||
|
showRemove: Boolean,
|
||||||
|
onRemove: () -> Unit,
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant),
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.padding(12.dp).fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
"Kind $kind",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
fontFamily = FontFamily.Monospace,
|
||||||
|
)
|
||||||
|
if (showRemove) {
|
||||||
|
IconButton(onClick = onRemove) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Delete,
|
||||||
|
contentDescription = stringResource(R.string.relay_management_remove),
|
||||||
|
tint = MaterialTheme.colorScheme.error,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun IpEntryCard(
|
||||||
|
ip: String,
|
||||||
|
reason: String?,
|
||||||
|
showRemove: Boolean,
|
||||||
|
onRemove: () -> Unit,
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant),
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.padding(12.dp).fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
Text(
|
||||||
|
ip,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
fontFamily = FontFamily.Monospace,
|
||||||
|
)
|
||||||
|
reason?.let {
|
||||||
|
Text(
|
||||||
|
it,
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (showRemove) {
|
||||||
|
IconButton(onClick = onRemove) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Close,
|
||||||
|
contentDescription = stringResource(R.string.relay_management_remove),
|
||||||
|
tint = MaterialTheme.colorScheme.error,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SettingsField(
|
||||||
|
label: String,
|
||||||
|
value: String,
|
||||||
|
onValueChange: (String) -> Unit,
|
||||||
|
onApply: () -> Unit,
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = value,
|
||||||
|
onValueChange = onValueChange,
|
||||||
|
label = { Text(label) },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
singleLine = true,
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
TextButton(
|
||||||
|
onClick = onApply,
|
||||||
|
modifier = Modifier.align(Alignment.End),
|
||||||
|
enabled = value.isNotBlank(),
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.relay_management_apply))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun HexInputDialog(
|
||||||
|
title: String,
|
||||||
|
label: String,
|
||||||
|
onConfirm: (String, String) -> Unit,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
) {
|
||||||
|
var hexValue by remember { mutableStateOf("") }
|
||||||
|
var reasonValue by remember { mutableStateOf("") }
|
||||||
|
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
title = { Text(title) },
|
||||||
|
text = {
|
||||||
|
Column {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = hexValue,
|
||||||
|
onValueChange = { hexValue = it },
|
||||||
|
label = { Text(label) },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
singleLine = true,
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
OutlinedTextField(
|
||||||
|
value = reasonValue,
|
||||||
|
onValueChange = { reasonValue = it },
|
||||||
|
label = { Text(stringResource(R.string.relay_management_reason_optional)) },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
singleLine = true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = { onConfirm(hexValue.trim(), reasonValue.trim()) },
|
||||||
|
enabled = hexValue.isNotBlank(),
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.relay_management_confirm))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = onDismiss) {
|
||||||
|
Text(stringResource(R.string.relay_management_cancel))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun KindInputDialog(
|
||||||
|
onConfirm: (Int) -> Unit,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
) {
|
||||||
|
var kindValue by remember { mutableStateOf("") }
|
||||||
|
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
title = { Text(stringResource(R.string.relay_management_allow_kind)) },
|
||||||
|
text = {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = kindValue,
|
||||||
|
onValueChange = { kindValue = it.filter { c -> c.isDigit() } },
|
||||||
|
label = { Text(stringResource(R.string.relay_management_kind_number)) },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
singleLine = true,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
kindValue.toIntOrNull()?.let { onConfirm(it) }
|
||||||
|
},
|
||||||
|
enabled = kindValue.toIntOrNull() != null,
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.relay_management_confirm))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = onDismiss) {
|
||||||
|
Text(stringResource(R.string.relay_management_cancel))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
+318
@@ -0,0 +1,318 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.nip86
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.vitorpamplona.amethyst.model.nip86RelayManagement.Nip86Retriever
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||||
|
import com.vitorpamplona.quartz.nip86RelayManagement.Nip86Client
|
||||||
|
import com.vitorpamplona.quartz.nip86RelayManagement.rpc.AllowedPubkey
|
||||||
|
import com.vitorpamplona.quartz.nip86RelayManagement.rpc.BannedEvent
|
||||||
|
import com.vitorpamplona.quartz.nip86RelayManagement.rpc.BannedPubkey
|
||||||
|
import com.vitorpamplona.quartz.nip86RelayManagement.rpc.BlockedIp
|
||||||
|
import com.vitorpamplona.quartz.nip86RelayManagement.rpc.EventNeedingModeration
|
||||||
|
import com.vitorpamplona.quartz.nip86RelayManagement.rpc.Nip86Request
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class RelayManagementViewModel(
|
||||||
|
relayUrl: NormalizedRelayUrl,
|
||||||
|
signer: NostrSigner,
|
||||||
|
private val retriever: Nip86Retriever,
|
||||||
|
) : ViewModel() {
|
||||||
|
val client = Nip86Client(relayUrl, signer)
|
||||||
|
|
||||||
|
private val _supportedMethods = MutableStateFlow<List<String>>(emptyList())
|
||||||
|
val supportedMethods: StateFlow<List<String>> = _supportedMethods
|
||||||
|
|
||||||
|
private val _bannedPubkeys = MutableStateFlow<List<BannedPubkey>>(emptyList())
|
||||||
|
val bannedPubkeys: StateFlow<List<BannedPubkey>> = _bannedPubkeys
|
||||||
|
|
||||||
|
private val _allowedPubkeys = MutableStateFlow<List<AllowedPubkey>>(emptyList())
|
||||||
|
val allowedPubkeys: StateFlow<List<AllowedPubkey>> = _allowedPubkeys
|
||||||
|
|
||||||
|
private val _bannedEvents = MutableStateFlow<List<BannedEvent>>(emptyList())
|
||||||
|
val bannedEvents: StateFlow<List<BannedEvent>> = _bannedEvents
|
||||||
|
|
||||||
|
private val _eventsNeedingModeration = MutableStateFlow<List<EventNeedingModeration>>(emptyList())
|
||||||
|
val eventsNeedingModeration: StateFlow<List<EventNeedingModeration>> = _eventsNeedingModeration
|
||||||
|
|
||||||
|
private val _allowedKinds = MutableStateFlow<List<Int>>(emptyList())
|
||||||
|
val allowedKinds: StateFlow<List<Int>> = _allowedKinds
|
||||||
|
|
||||||
|
private val _blockedIps = MutableStateFlow<List<BlockedIp>>(emptyList())
|
||||||
|
val blockedIps: StateFlow<List<BlockedIp>> = _blockedIps
|
||||||
|
|
||||||
|
private val _isLoading = MutableStateFlow(false)
|
||||||
|
val isLoading: StateFlow<Boolean> = _isLoading
|
||||||
|
|
||||||
|
private val _error = MutableStateFlow<String?>(null)
|
||||||
|
val error: StateFlow<String?> = _error
|
||||||
|
|
||||||
|
fun loadSupportedMethods() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
_isLoading.value = true
|
||||||
|
_error.value = null
|
||||||
|
val response = retriever.execute(client, Nip86Request.supportedMethods())
|
||||||
|
if (response.error != null) {
|
||||||
|
_error.value = response.error
|
||||||
|
} else {
|
||||||
|
_supportedMethods.value = client.parseSupportedMethods(response) ?: emptyList()
|
||||||
|
}
|
||||||
|
_isLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadBannedPubkeys() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val response = retriever.execute(client, Nip86Request.listBannedPubkeys())
|
||||||
|
if (response.error != null) {
|
||||||
|
_error.value = response.error
|
||||||
|
} else {
|
||||||
|
_bannedPubkeys.value = client.parseBannedPubkeys(response) ?: emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadAllowedPubkeys() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val response = retriever.execute(client, Nip86Request.listAllowedPubkeys())
|
||||||
|
if (response.error != null) {
|
||||||
|
_error.value = response.error
|
||||||
|
} else {
|
||||||
|
_allowedPubkeys.value = client.parseAllowedPubkeys(response) ?: emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadBannedEvents() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val response = retriever.execute(client, Nip86Request.listBannedEvents())
|
||||||
|
if (response.error != null) {
|
||||||
|
_error.value = response.error
|
||||||
|
} else {
|
||||||
|
_bannedEvents.value = client.parseBannedEvents(response) ?: emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadEventsNeedingModeration() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val response = retriever.execute(client, Nip86Request.listEventsNeedingModeration())
|
||||||
|
if (response.error != null) {
|
||||||
|
_error.value = response.error
|
||||||
|
} else {
|
||||||
|
_eventsNeedingModeration.value = client.parseEventsNeedingModeration(response) ?: emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadAllowedKinds() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val response = retriever.execute(client, Nip86Request.listAllowedKinds())
|
||||||
|
if (response.error != null) {
|
||||||
|
_error.value = response.error
|
||||||
|
} else {
|
||||||
|
_allowedKinds.value = client.parseAllowedKinds(response) ?: emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadBlockedIps() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val response = retriever.execute(client, Nip86Request.listBlockedIps())
|
||||||
|
if (response.error != null) {
|
||||||
|
_error.value = response.error
|
||||||
|
} else {
|
||||||
|
_blockedIps.value = client.parseBlockedIps(response) ?: emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun banPubkey(
|
||||||
|
pubkey: String,
|
||||||
|
reason: String? = null,
|
||||||
|
) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val response = retriever.execute(client, Nip86Request.banPubkey(pubkey, reason))
|
||||||
|
if (response.error != null) {
|
||||||
|
_error.value = response.error
|
||||||
|
} else {
|
||||||
|
loadBannedPubkeys()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun unbanPubkey(pubkey: String) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val response = retriever.execute(client, Nip86Request.unbanPubkey(pubkey))
|
||||||
|
if (response.error != null) {
|
||||||
|
_error.value = response.error
|
||||||
|
} else {
|
||||||
|
loadBannedPubkeys()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun allowPubkey(
|
||||||
|
pubkey: String,
|
||||||
|
reason: String? = null,
|
||||||
|
) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val response = retriever.execute(client, Nip86Request.allowPubkey(pubkey, reason))
|
||||||
|
if (response.error != null) {
|
||||||
|
_error.value = response.error
|
||||||
|
} else {
|
||||||
|
loadAllowedPubkeys()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun unallowPubkey(pubkey: String) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val response = retriever.execute(client, Nip86Request.unallowPubkey(pubkey))
|
||||||
|
if (response.error != null) {
|
||||||
|
_error.value = response.error
|
||||||
|
} else {
|
||||||
|
loadAllowedPubkeys()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun banEvent(
|
||||||
|
eventId: String,
|
||||||
|
reason: String? = null,
|
||||||
|
) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val response = retriever.execute(client, Nip86Request.banEvent(eventId, reason))
|
||||||
|
if (response.error != null) {
|
||||||
|
_error.value = response.error
|
||||||
|
} else {
|
||||||
|
loadBannedEvents()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun allowEvent(
|
||||||
|
eventId: String,
|
||||||
|
reason: String? = null,
|
||||||
|
) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val response = retriever.execute(client, Nip86Request.allowEvent(eventId, reason))
|
||||||
|
if (response.error != null) {
|
||||||
|
_error.value = response.error
|
||||||
|
} else {
|
||||||
|
loadEventsNeedingModeration()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun changeRelayName(newName: String) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val response = retriever.execute(client, Nip86Request.changeRelayName(newName))
|
||||||
|
if (response.error != null) {
|
||||||
|
_error.value = response.error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun changeRelayDescription(newDescription: String) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val response = retriever.execute(client, Nip86Request.changeRelayDescription(newDescription))
|
||||||
|
if (response.error != null) {
|
||||||
|
_error.value = response.error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun changeRelayIcon(newIconUrl: String) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val response = retriever.execute(client, Nip86Request.changeRelayIcon(newIconUrl))
|
||||||
|
if (response.error != null) {
|
||||||
|
_error.value = response.error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun allowKind(kind: Int) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val response = retriever.execute(client, Nip86Request.allowKind(kind))
|
||||||
|
if (response.error != null) {
|
||||||
|
_error.value = response.error
|
||||||
|
} else {
|
||||||
|
loadAllowedKinds()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun disallowKind(kind: Int) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val response = retriever.execute(client, Nip86Request.disallowKind(kind))
|
||||||
|
if (response.error != null) {
|
||||||
|
_error.value = response.error
|
||||||
|
} else {
|
||||||
|
loadAllowedKinds()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun blockIp(
|
||||||
|
ip: String,
|
||||||
|
reason: String? = null,
|
||||||
|
) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val response = retriever.execute(client, Nip86Request.blockIp(ip, reason))
|
||||||
|
if (response.error != null) {
|
||||||
|
_error.value = response.error
|
||||||
|
} else {
|
||||||
|
loadBlockedIps()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun unblockIp(ip: String) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val response = retriever.execute(client, Nip86Request.unblockIp(ip))
|
||||||
|
if (response.error != null) {
|
||||||
|
_error.value = response.error
|
||||||
|
} else {
|
||||||
|
loadBlockedIps()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearError() {
|
||||||
|
_error.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadAllLists() {
|
||||||
|
val methods = _supportedMethods.value
|
||||||
|
if (methods.contains("listbannedpubkeys")) loadBannedPubkeys()
|
||||||
|
if (methods.contains("listallowedpubkeys")) loadAllowedPubkeys()
|
||||||
|
if (methods.contains("listbannedevents")) loadBannedEvents()
|
||||||
|
if (methods.contains("listeventsneedingmoderation")) loadEventsNeedingModeration()
|
||||||
|
if (methods.contains("listallowedkinds")) loadAllowedKinds()
|
||||||
|
if (methods.contains("listblockedips")) loadBlockedIps()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1974,4 +1974,49 @@
|
|||||||
<string name="web_bookmark_delete">Delete</string>
|
<string name="web_bookmark_delete">Delete</string>
|
||||||
<string name="web_bookmark_delete_confirm">Delete this web bookmark?</string>
|
<string name="web_bookmark_delete_confirm">Delete this web bookmark?</string>
|
||||||
<string name="web_bookmark_open_url">Open URL</string>
|
<string name="web_bookmark_open_url">Open URL</string>
|
||||||
|
|
||||||
|
<!-- NIP-86 Relay Management -->
|
||||||
|
<string name="relay_management_title">Manage %1$s</string>
|
||||||
|
<string name="relay_management_loading">Loading relay management capabilities…</string>
|
||||||
|
<string name="relay_management_error">Unable to connect to relay management</string>
|
||||||
|
<string name="relay_management_no_methods">No management methods available</string>
|
||||||
|
<string name="relay_management_dismiss">Dismiss</string>
|
||||||
|
<string name="relay_management_tab_pubkeys">Pubkeys</string>
|
||||||
|
<string name="relay_management_tab_events">Events</string>
|
||||||
|
<string name="relay_management_tab_kinds">Kinds</string>
|
||||||
|
<string name="relay_management_tab_ips">IPs</string>
|
||||||
|
<string name="relay_management_tab_settings">Settings</string>
|
||||||
|
<string name="relay_management_banned_pubkeys">Banned Pubkeys</string>
|
||||||
|
<string name="relay_management_no_banned_pubkeys">No banned pubkeys</string>
|
||||||
|
<string name="relay_management_allowed_pubkeys">Allowed Pubkeys</string>
|
||||||
|
<string name="relay_management_no_allowed_pubkeys">No allowed pubkeys</string>
|
||||||
|
<string name="relay_management_moderation_queue">Moderation Queue</string>
|
||||||
|
<string name="relay_management_no_moderation_events">No events needing moderation</string>
|
||||||
|
<string name="relay_management_banned_events">Banned Events</string>
|
||||||
|
<string name="relay_management_no_banned_events">No banned events</string>
|
||||||
|
<string name="relay_management_allowed_kinds">Allowed Kinds</string>
|
||||||
|
<string name="relay_management_no_allowed_kinds">No allowed kinds</string>
|
||||||
|
<string name="relay_management_blocked_ips">Blocked IPs</string>
|
||||||
|
<string name="relay_management_no_blocked_ips">No blocked IPs</string>
|
||||||
|
<string name="relay_management_add">Add</string>
|
||||||
|
<string name="relay_management_remove">Remove</string>
|
||||||
|
<string name="relay_management_allow">Allow</string>
|
||||||
|
<string name="relay_management_ban">Ban</string>
|
||||||
|
<string name="relay_management_ban_pubkey">Ban Pubkey</string>
|
||||||
|
<string name="relay_management_allow_pubkey">Allow Pubkey</string>
|
||||||
|
<string name="relay_management_ban_event">Ban Event</string>
|
||||||
|
<string name="relay_management_allow_kind">Allow Kind</string>
|
||||||
|
<string name="relay_management_block_ip">Block IP</string>
|
||||||
|
<string name="relay_management_pubkey_hex">Pubkey (hex)</string>
|
||||||
|
<string name="relay_management_event_id_hex">Event ID (hex)</string>
|
||||||
|
<string name="relay_management_kind_number">Kind number</string>
|
||||||
|
<string name="relay_management_ip_address">IP address</string>
|
||||||
|
<string name="relay_management_reason_optional">Reason (optional)</string>
|
||||||
|
<string name="relay_management_confirm">Confirm</string>
|
||||||
|
<string name="relay_management_cancel">Cancel</string>
|
||||||
|
<string name="relay_management_apply">Apply</string>
|
||||||
|
<string name="relay_management_relay_name">Relay Name</string>
|
||||||
|
<string name="relay_management_relay_description">Relay Description</string>
|
||||||
|
<string name="relay_management_relay_icon_url">Relay Icon URL</string>
|
||||||
|
<string name="relay_management_button">Manage Relay</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
+168
@@ -0,0 +1,168 @@
|
|||||||
|
/*
|
||||||
|
* 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.nip86RelayManagement
|
||||||
|
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.JsonMapper
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.toHttp
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||||
|
import com.vitorpamplona.quartz.nip86RelayManagement.rpc.AllowedPubkey
|
||||||
|
import com.vitorpamplona.quartz.nip86RelayManagement.rpc.BannedEvent
|
||||||
|
import com.vitorpamplona.quartz.nip86RelayManagement.rpc.BannedPubkey
|
||||||
|
import com.vitorpamplona.quartz.nip86RelayManagement.rpc.BlockedIp
|
||||||
|
import com.vitorpamplona.quartz.nip86RelayManagement.rpc.EventNeedingModeration
|
||||||
|
import com.vitorpamplona.quartz.nip86RelayManagement.rpc.Nip86Request
|
||||||
|
import com.vitorpamplona.quartz.nip86RelayManagement.rpc.Nip86Response
|
||||||
|
import com.vitorpamplona.quartz.nip98HttpAuth.HTTPAuthorizationEvent
|
||||||
|
import kotlinx.serialization.json.JsonArray
|
||||||
|
import kotlinx.serialization.json.JsonPrimitive
|
||||||
|
import kotlinx.serialization.json.boolean
|
||||||
|
import kotlinx.serialization.json.int
|
||||||
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
|
|
||||||
|
class Nip86Client(
|
||||||
|
val relayUrl: NormalizedRelayUrl,
|
||||||
|
val signer: NostrSigner,
|
||||||
|
) {
|
||||||
|
val httpUrl: String = relayUrl.toHttp()
|
||||||
|
|
||||||
|
suspend fun buildAuthHeader(payload: ByteArray): String {
|
||||||
|
val template =
|
||||||
|
HTTPAuthorizationEvent.build(
|
||||||
|
url = httpUrl,
|
||||||
|
method = "POST",
|
||||||
|
file = payload,
|
||||||
|
)
|
||||||
|
val signedEvent = signer.sign<HTTPAuthorizationEvent>(template)
|
||||||
|
return signedEvent.toAuthToken()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun serializeRequest(request: Nip86Request): String = JsonMapper.toJson(request)
|
||||||
|
|
||||||
|
fun parseResponse(json: String): Nip86Response = JsonMapper.fromJson<Nip86Response>(json)
|
||||||
|
|
||||||
|
fun parseSupportedMethods(response: Nip86Response): List<String>? {
|
||||||
|
val result = response.result ?: return null
|
||||||
|
return (result as? JsonArray)?.map { it.jsonPrimitive.content }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun parseBooleanResult(response: Nip86Response): Boolean? {
|
||||||
|
val result = response.result ?: return null
|
||||||
|
return (result as? JsonPrimitive)?.boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
fun parseBannedPubkeys(response: Nip86Response): List<BannedPubkey>? {
|
||||||
|
val result = response.result ?: return null
|
||||||
|
return JsonMapper.fromJson<List<BannedPubkey>>(result.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun parseAllowedPubkeys(response: Nip86Response): List<AllowedPubkey>? {
|
||||||
|
val result = response.result ?: return null
|
||||||
|
return JsonMapper.fromJson<List<AllowedPubkey>>(result.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun parseBannedEvents(response: Nip86Response): List<BannedEvent>? {
|
||||||
|
val result = response.result ?: return null
|
||||||
|
return JsonMapper.fromJson<List<BannedEvent>>(result.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun parseEventsNeedingModeration(response: Nip86Response): List<EventNeedingModeration>? {
|
||||||
|
val result = response.result ?: return null
|
||||||
|
return JsonMapper.fromJson<List<EventNeedingModeration>>(result.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun parseAllowedKinds(response: Nip86Response): List<Int>? {
|
||||||
|
val result = response.result ?: return null
|
||||||
|
return (result as? JsonArray)?.map { it.jsonPrimitive.int }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun parseBlockedIps(response: Nip86Response): List<BlockedIp>? {
|
||||||
|
val result = response.result ?: return null
|
||||||
|
return JsonMapper.fromJson<List<BlockedIp>>(result.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convenience methods that build requests
|
||||||
|
|
||||||
|
fun supportedMethodsRequest() = Nip86Request.supportedMethods()
|
||||||
|
|
||||||
|
fun banPubkeyRequest(
|
||||||
|
pubkey: String,
|
||||||
|
reason: String? = null,
|
||||||
|
) = Nip86Request.banPubkey(pubkey, reason)
|
||||||
|
|
||||||
|
fun unbanPubkeyRequest(
|
||||||
|
pubkey: String,
|
||||||
|
reason: String? = null,
|
||||||
|
) = Nip86Request.unbanPubkey(pubkey, reason)
|
||||||
|
|
||||||
|
fun listBannedPubkeysRequest() = Nip86Request.listBannedPubkeys()
|
||||||
|
|
||||||
|
fun allowPubkeyRequest(
|
||||||
|
pubkey: String,
|
||||||
|
reason: String? = null,
|
||||||
|
) = Nip86Request.allowPubkey(pubkey, reason)
|
||||||
|
|
||||||
|
fun unallowPubkeyRequest(
|
||||||
|
pubkey: String,
|
||||||
|
reason: String? = null,
|
||||||
|
) = Nip86Request.unallowPubkey(pubkey, reason)
|
||||||
|
|
||||||
|
fun listAllowedPubkeysRequest() = Nip86Request.listAllowedPubkeys()
|
||||||
|
|
||||||
|
fun listEventsNeedingModerationRequest() = Nip86Request.listEventsNeedingModeration()
|
||||||
|
|
||||||
|
fun allowEventRequest(
|
||||||
|
eventId: String,
|
||||||
|
reason: String? = null,
|
||||||
|
) = Nip86Request.allowEvent(eventId, reason)
|
||||||
|
|
||||||
|
fun banEventRequest(
|
||||||
|
eventId: String,
|
||||||
|
reason: String? = null,
|
||||||
|
) = Nip86Request.banEvent(eventId, reason)
|
||||||
|
|
||||||
|
fun listBannedEventsRequest() = Nip86Request.listBannedEvents()
|
||||||
|
|
||||||
|
fun changeRelayNameRequest(newName: String) = Nip86Request.changeRelayName(newName)
|
||||||
|
|
||||||
|
fun changeRelayDescriptionRequest(newDescription: String) = Nip86Request.changeRelayDescription(newDescription)
|
||||||
|
|
||||||
|
fun changeRelayIconRequest(newIconUrl: String) = Nip86Request.changeRelayIcon(newIconUrl)
|
||||||
|
|
||||||
|
fun allowKindRequest(kind: Int) = Nip86Request.allowKind(kind)
|
||||||
|
|
||||||
|
fun disallowKindRequest(kind: Int) = Nip86Request.disallowKind(kind)
|
||||||
|
|
||||||
|
fun listAllowedKindsRequest() = Nip86Request.listAllowedKinds()
|
||||||
|
|
||||||
|
fun blockIpRequest(
|
||||||
|
ip: String,
|
||||||
|
reason: String? = null,
|
||||||
|
) = Nip86Request.blockIp(ip, reason)
|
||||||
|
|
||||||
|
fun unblockIpRequest(ip: String) = Nip86Request.unblockIp(ip)
|
||||||
|
|
||||||
|
fun listBlockedIpsRequest() = Nip86Request.listBlockedIps()
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun supportsNip86(supportedNips: List<String>?): Boolean = supportedNips?.any { it == "86" } == true
|
||||||
|
}
|
||||||
|
}
|
||||||
+44
@@ -0,0 +1,44 @@
|
|||||||
|
/*
|
||||||
|
* 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.nip86RelayManagement.rpc
|
||||||
|
|
||||||
|
object Nip86Method {
|
||||||
|
const val SUPPORTED_METHODS = "supportedmethods"
|
||||||
|
const val BAN_PUBKEY = "banpubkey"
|
||||||
|
const val UNBAN_PUBKEY = "unbanpubkey"
|
||||||
|
const val LIST_BANNED_PUBKEYS = "listbannedpubkeys"
|
||||||
|
const val ALLOW_PUBKEY = "allowpubkey"
|
||||||
|
const val UNALLOW_PUBKEY = "unallowpubkey"
|
||||||
|
const val LIST_ALLOWED_PUBKEYS = "listallowedpubkeys"
|
||||||
|
const val LIST_EVENTS_NEEDING_MODERATION = "listeventsneedingmoderation"
|
||||||
|
const val ALLOW_EVENT = "allowevent"
|
||||||
|
const val BAN_EVENT = "banevent"
|
||||||
|
const val LIST_BANNED_EVENTS = "listbannedevents"
|
||||||
|
const val CHANGE_RELAY_NAME = "changerelayname"
|
||||||
|
const val CHANGE_RELAY_DESCRIPTION = "changerelaydescription"
|
||||||
|
const val CHANGE_RELAY_ICON = "changerelayicon"
|
||||||
|
const val ALLOW_KIND = "allowkind"
|
||||||
|
const val DISALLOW_KIND = "disallowkind"
|
||||||
|
const val LIST_ALLOWED_KINDS = "listallowedkinds"
|
||||||
|
const val BLOCK_IP = "blockip"
|
||||||
|
const val UNBLOCK_IP = "unblockip"
|
||||||
|
const val LIST_BLOCKED_IPS = "listblockedips"
|
||||||
|
}
|
||||||
+170
@@ -0,0 +1,170 @@
|
|||||||
|
/*
|
||||||
|
* 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.nip86RelayManagement.rpc
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.json.JsonArray
|
||||||
|
import kotlinx.serialization.json.JsonPrimitive
|
||||||
|
import kotlinx.serialization.json.buildJsonArray
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class Nip86Request(
|
||||||
|
val method: String,
|
||||||
|
val params: JsonArray = JsonArray(emptyList()),
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
fun supportedMethods() =
|
||||||
|
Nip86Request(
|
||||||
|
method = Nip86Method.SUPPORTED_METHODS,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun banPubkey(
|
||||||
|
pubkey: String,
|
||||||
|
reason: String? = null,
|
||||||
|
) = Nip86Request(
|
||||||
|
method = Nip86Method.BAN_PUBKEY,
|
||||||
|
params = buildParams(pubkey, reason),
|
||||||
|
)
|
||||||
|
|
||||||
|
fun unbanPubkey(
|
||||||
|
pubkey: String,
|
||||||
|
reason: String? = null,
|
||||||
|
) = Nip86Request(
|
||||||
|
method = Nip86Method.UNBAN_PUBKEY,
|
||||||
|
params = buildParams(pubkey, reason),
|
||||||
|
)
|
||||||
|
|
||||||
|
fun listBannedPubkeys() =
|
||||||
|
Nip86Request(
|
||||||
|
method = Nip86Method.LIST_BANNED_PUBKEYS,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun allowPubkey(
|
||||||
|
pubkey: String,
|
||||||
|
reason: String? = null,
|
||||||
|
) = Nip86Request(
|
||||||
|
method = Nip86Method.ALLOW_PUBKEY,
|
||||||
|
params = buildParams(pubkey, reason),
|
||||||
|
)
|
||||||
|
|
||||||
|
fun unallowPubkey(
|
||||||
|
pubkey: String,
|
||||||
|
reason: String? = null,
|
||||||
|
) = Nip86Request(
|
||||||
|
method = Nip86Method.UNALLOW_PUBKEY,
|
||||||
|
params = buildParams(pubkey, reason),
|
||||||
|
)
|
||||||
|
|
||||||
|
fun listAllowedPubkeys() =
|
||||||
|
Nip86Request(
|
||||||
|
method = Nip86Method.LIST_ALLOWED_PUBKEYS,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun listEventsNeedingModeration() =
|
||||||
|
Nip86Request(
|
||||||
|
method = Nip86Method.LIST_EVENTS_NEEDING_MODERATION,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun allowEvent(
|
||||||
|
eventId: String,
|
||||||
|
reason: String? = null,
|
||||||
|
) = Nip86Request(
|
||||||
|
method = Nip86Method.ALLOW_EVENT,
|
||||||
|
params = buildParams(eventId, reason),
|
||||||
|
)
|
||||||
|
|
||||||
|
fun banEvent(
|
||||||
|
eventId: String,
|
||||||
|
reason: String? = null,
|
||||||
|
) = Nip86Request(
|
||||||
|
method = Nip86Method.BAN_EVENT,
|
||||||
|
params = buildParams(eventId, reason),
|
||||||
|
)
|
||||||
|
|
||||||
|
fun listBannedEvents() =
|
||||||
|
Nip86Request(
|
||||||
|
method = Nip86Method.LIST_BANNED_EVENTS,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun changeRelayName(newName: String) =
|
||||||
|
Nip86Request(
|
||||||
|
method = Nip86Method.CHANGE_RELAY_NAME,
|
||||||
|
params = buildJsonArray { add(JsonPrimitive(newName)) },
|
||||||
|
)
|
||||||
|
|
||||||
|
fun changeRelayDescription(newDescription: String) =
|
||||||
|
Nip86Request(
|
||||||
|
method = Nip86Method.CHANGE_RELAY_DESCRIPTION,
|
||||||
|
params = buildJsonArray { add(JsonPrimitive(newDescription)) },
|
||||||
|
)
|
||||||
|
|
||||||
|
fun changeRelayIcon(newIconUrl: String) =
|
||||||
|
Nip86Request(
|
||||||
|
method = Nip86Method.CHANGE_RELAY_ICON,
|
||||||
|
params = buildJsonArray { add(JsonPrimitive(newIconUrl)) },
|
||||||
|
)
|
||||||
|
|
||||||
|
fun allowKind(kind: Int) =
|
||||||
|
Nip86Request(
|
||||||
|
method = Nip86Method.ALLOW_KIND,
|
||||||
|
params = buildJsonArray { add(JsonPrimitive(kind)) },
|
||||||
|
)
|
||||||
|
|
||||||
|
fun disallowKind(kind: Int) =
|
||||||
|
Nip86Request(
|
||||||
|
method = Nip86Method.DISALLOW_KIND,
|
||||||
|
params = buildJsonArray { add(JsonPrimitive(kind)) },
|
||||||
|
)
|
||||||
|
|
||||||
|
fun listAllowedKinds() =
|
||||||
|
Nip86Request(
|
||||||
|
method = Nip86Method.LIST_ALLOWED_KINDS,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun blockIp(
|
||||||
|
ip: String,
|
||||||
|
reason: String? = null,
|
||||||
|
) = Nip86Request(
|
||||||
|
method = Nip86Method.BLOCK_IP,
|
||||||
|
params = buildParams(ip, reason),
|
||||||
|
)
|
||||||
|
|
||||||
|
fun unblockIp(ip: String) =
|
||||||
|
Nip86Request(
|
||||||
|
method = Nip86Method.UNBLOCK_IP,
|
||||||
|
params = buildJsonArray { add(JsonPrimitive(ip)) },
|
||||||
|
)
|
||||||
|
|
||||||
|
fun listBlockedIps() =
|
||||||
|
Nip86Request(
|
||||||
|
method = Nip86Method.LIST_BLOCKED_IPS,
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun buildParams(
|
||||||
|
primary: String,
|
||||||
|
reason: String? = null,
|
||||||
|
): JsonArray =
|
||||||
|
buildJsonArray {
|
||||||
|
add(JsonPrimitive(primary))
|
||||||
|
reason?.let { add(JsonPrimitive(it)) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+60
@@ -0,0 +1,60 @@
|
|||||||
|
/*
|
||||||
|
* 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.nip86RelayManagement.rpc
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.json.JsonElement
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class Nip86Response(
|
||||||
|
val result: JsonElement? = null,
|
||||||
|
val error: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class BannedPubkey(
|
||||||
|
val pubkey: String,
|
||||||
|
val reason: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class AllowedPubkey(
|
||||||
|
val pubkey: String,
|
||||||
|
val reason: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class BannedEvent(
|
||||||
|
val id: String,
|
||||||
|
val reason: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class EventNeedingModeration(
|
||||||
|
val id: String,
|
||||||
|
val reason: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class BlockedIp(
|
||||||
|
val ip: String,
|
||||||
|
val reason: String? = null,
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user