mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-11-08 08:01:09 +00:00
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:
parent
1f24973b06
commit
ee75f4b5f7
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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"))
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user