Resolve block commands in the active geohash

Use the current channel's sender identities for block and ban commands.
Reject ambiguous names and show the result in the channel.
This commit is contained in:
Taksh 2026-11-07 17:03:24 +05:30
parent 1f24973b06
commit ee75f4b5f7
5 changed files with 101 additions and 22 deletions

View File

@ -69,6 +69,29 @@ class GeohashRepository(
return matches.singleOrNull()?.key
}
/** Resolve a block command within the displayed channel, including anonymous senders. */
fun findPubkeyForGeohashBlock(geohash: String, targetNickname: String): String? {
val base = targetNickname.substringBefore('#')
val suffix = targetNickname.substringAfter('#', missingDelimiterValue = "")
if (base.isEmpty() || ('#' in targetNickname && suffix.length != 4)) return null
fun matches(pubkey: String, name: String): Boolean =
name.substringBefore('#').equals(base, ignoreCase = true) &&
(suffix.isEmpty() || pubkey.endsWith(suffix, ignoreCase = true))
val snapshot = participantStore.snapshots.value
val candidates = mutableSetOf<String>()
for (pubkey in snapshot.participants[geohash].orEmpty().keys) {
if (matches(pubkey, snapshot.nicknames[pubkey] ?: "anon")) candidates.add(pubkey)
}
// A person can age out of presence while their message is still visible.
for (message in state.getChannelMessagesValue()["geo:$geohash"].orEmpty()) {
val pubkey = message.senderNostrPubkey?.lowercase() ?: continue
if (matches(pubkey, message.originalSender ?: message.sender)) candidates.add(pubkey)
}
return candidates.singleOrNull()
}
@Synchronized
fun findPubkeyByShortId(shortId: String): String? {
// First check cached nicknames (fastest)

View File

@ -20,7 +20,7 @@ class CommandProcessor(
// Available commands list
private val baseCommands = listOf(
CommandSuggestion("/block", emptyList(), "[nickname]", "block or list blocked peers"),
CommandSuggestion("/block", listOf("/ban"), "[nickname]", "block or list blocked peers"),
CommandSuggestion("/channels", emptyList(), null, "show all discovered channels"),
CommandSuggestion("/clear", emptyList(), null, "clear chat messages"),
CommandSuggestion("/hug", emptyList(), "<nickname>", "send someone a warm hug"),
@ -46,7 +46,7 @@ class CommandProcessor(
"/w" -> handleWhoCommand(meshService, viewModel)
"/clear" -> handleClearCommand()
"/pass" -> handlePassCommand(parts, myPeerID)
"/block" -> handleBlockCommand(parts, meshService)
"/block", "/ban" -> handleBlockCommand(parts, meshService, viewModel)
"/unblock" -> handleUnblockCommand(parts, meshService)
"/hug" -> handleActionCommand(parts, "gives", "a warm hug 🫂", meshService, myPeerID, onSendMessage, viewModel)
"/slap" -> handleActionCommand(parts, "slaps", "around a bit with a large trout 🐟", meshService, myPeerID, onSendMessage, viewModel)
@ -256,9 +256,15 @@ class CommandProcessor(
}
}
private fun handleBlockCommand(parts: List<String>, meshService: MeshService) {
if (parts.size > 1) {
val targetName = parts[1].removePrefix("@")
private fun handleBlockCommand(parts: List<String>, meshService: MeshService, viewModel: ChatViewModel?) {
val targetName = parts.drop(1).firstOrNull { it.isNotBlank() }?.removePrefix("@")
if (!targetName.isNullOrEmpty()) {
if (state.selectedLocationChannel.value is com.bitchat.android.geohash.ChannelID.Location &&
state.getSelectedPrivateChatPeerValue() == null
) {
viewModel?.blockUserInGeohash(targetName)
return
}
privateChatManager.blockPeerByNickname(targetName, meshService)
} else {
// List blocked users

View File

@ -285,30 +285,28 @@ class GeohashViewModel(
fun getNostrKeyMapping(): Map<String, String> = repo.getNostrKeyMapping()
fun blockUserInGeohash(targetNickname: String, pubkeyHex: String? = null) {
val selected = state.selectedLocationChannel.value as? com.bitchat.android.geohash.ChannelID.Location
val geohash = selected?.channel?.geohash
val pubkey = pubkeyHex?.takeIf { it.isNotBlank() }?.lowercase()
?: repo.findPubkeyByNickname(targetNickname)
if (pubkey != null) {
?: geohash?.let { repo.findPubkeyForGeohashBlock(it, targetNickname) }
val feedback = if (pubkey != null) {
dataManager.addGeohashBlockedUser(pubkey)
messageManager.removeGeohashMessagesFromSender(pubkey)
// Refresh people list and counts to remove blocked entry immediately
repo.refreshGeohashPeople()
repo.updateReactiveParticipantCounts()
val sysMsg = com.bitchat.android.model.BitchatMessage(
sender = "system",
content = "blocked $targetNickname in geohash channels",
timestamp = Date(),
isRelay = false
)
messageManager.addMessage(sysMsg)
"blocked $targetNickname in geohash channels"
} else {
val sysMsg = com.bitchat.android.model.BitchatMessage(
sender = "system",
content = "user '$targetNickname' not found in current geohash",
timestamp = Date(),
isRelay = false
)
messageManager.addMessage(sysMsg)
"peer '$targetNickname' not found or ambiguous in this geohash"
}
val message = com.bitchat.android.model.BitchatMessage(
sender = "system",
content = feedback,
timestamp = Date(),
isRelay = false
)
if (geohash != null) messageManager.addChannelMessage("geo:$geohash", message)
else messageManager.addMessage(message)
}
fun selectLocationChannel(channel: com.bitchat.android.geohash.ChannelID) {

View File

@ -19,6 +19,7 @@ import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito
import org.mockito.kotlin.mock
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
import org.robolectric.RobolectricTestRunner
import java.util.Date
@ -142,6 +143,25 @@ class CommandProcessorTest {
assertTrue(locallyRead.contains(message.id))
}
@Test
fun `block and ban in a geohash use the geohash block handler`() {
chatState.setSelectedLocationChannel(
ChannelID.Location(GeohashChannel(GeohashChannelLevel.PROVINCE, "0000"))
)
val viewModel: ChatViewModel = mock()
for (command in listOf("/block", "/ban")) {
assertTrue(commandProcessor.processCommand(
command = "$command @anon#a1b2",
meshService = meshService,
myPeerID = "self",
onSendMessage = { _, _, _ -> error("blocking must not publish a message") },
viewModel = viewModel
))
}
verify(viewModel, Mockito.times(2)).blockUserInGeohash("anon#a1b2", null)
verify(meshService, Mockito.never()).getPeerNicknames()
}
@Test
fun `pay feedback is added to active geohash channel`() {
val geohash = "u0nd"

View File

@ -2,6 +2,7 @@ package com.bitchat.android.nostr
import android.app.Application
import androidx.test.core.app.ApplicationProvider
import com.bitchat.android.model.BitchatMessage
import com.bitchat.android.services.AppStateStore
import com.bitchat.android.ui.ChatState
import com.bitchat.android.ui.DataManager
@ -16,6 +17,7 @@ import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import java.util.Date
@RunWith(RobolectricTestRunner::class)
class GeohashNicknameLookupTest {
@ -26,13 +28,15 @@ class GeohashNicknameLookupTest {
private val bob = "c".repeat(60) + "3333"
private val store = GeohashParticipantStore { 0L }
private lateinit var scope: CoroutineScope
private lateinit var state: ChatState
private lateinit var repo: GeohashRepository
@Before
fun setUp() {
AppStateStore.clear()
scope = CoroutineScope(SupervisorJob() + Dispatchers.Unconfined)
repo = GeohashRepository(application, ChatState(scope), DataManager(application), scope, store)
state = ChatState(scope)
repo = GeohashRepository(application, state, DataManager(application), scope, store)
store.cacheNickname(firstAlice, "fixture-alice")
store.cacheNickname(secondAlice, "fixture-alice")
store.cacheNickname(bob, "fixture-bob")
@ -65,4 +69,32 @@ class GeohashNicknameLookupTest {
assertNull(repo.findPubkeyByNickname("fixture-alice#3333"))
assertNull(repo.findPubkeyByNickname("anon"))
}
@Test
fun `block target is restricted to the selected geohash and fails closed on collisions`() {
store.updateParticipant("0000", firstAlice, 0)
store.updateParticipant("0001", secondAlice, 0)
assertEquals(firstAlice, repo.findPubkeyForGeohashBlock("0000", "fixture-alice"))
assertNull(repo.findPubkeyForGeohashBlock("0000", "fixture-alice#2222"))
store.updateParticipant("0000", secondAlice, 0)
assertNull(repo.findPubkeyForGeohashBlock("0000", "fixture-alice"))
assertEquals(secondAlice, repo.findPubkeyForGeohashBlock("0000", "fixture-alice#2222"))
}
@Test
fun `anonymous sender can be blocked from a retained chat message without presence`() {
val pubkey = "d".repeat(60) + "a1b2"
state.setChannelMessages(mapOf("geo:0000" to listOf(BitchatMessage(
sender = "anon",
content = "fixture message",
timestamp = Date(0),
originalSender = "anon#a1b2",
senderNostrPubkey = pubkey
))))
assertEquals(pubkey, repo.findPubkeyForGeohashBlock("0000", "anon#A1B2"))
assertNull(repo.findPubkeyForGeohashBlock("0001", "anon#a1b2"))
}
}