mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 23:05:20 +00:00
Surface terminal Nostr DM failures
This commit is contained in:
@@ -317,16 +317,40 @@ final class NostrTransport: Transport, @unchecked Sendable {
|
||||
|
||||
func sendPrivateMessage(_ content: String, to peerID: PeerID, recipientNickname: String, messageID: String) {
|
||||
Task { @MainActor in
|
||||
guard let recipientNpub = resolveRecipientNpub(for: peerID),
|
||||
let recipientHex = npubToHex(recipientNpub),
|
||||
let senderIdentity = try? dependencies.currentIdentity() else { return }
|
||||
let failurePolicy = PrivateEnvelopeFailurePolicy.userMessage(
|
||||
messageID: messageID
|
||||
)
|
||||
guard let recipientNpub = resolveRecipientNpub(for: peerID) else {
|
||||
handlePrivateEnvelopeFailure(
|
||||
events: [],
|
||||
registerPending: false,
|
||||
policy: failurePolicy
|
||||
)
|
||||
return
|
||||
}
|
||||
guard let recipientHex = npubToHex(recipientNpub) else {
|
||||
handlePrivateEnvelopeFailure(
|
||||
events: [],
|
||||
registerPending: false,
|
||||
policy: failurePolicy
|
||||
)
|
||||
return
|
||||
}
|
||||
guard let senderIdentity = try? dependencies.currentIdentity() else {
|
||||
handlePrivateEnvelopeFailure(
|
||||
events: [],
|
||||
registerPending: false,
|
||||
policy: failurePolicy
|
||||
)
|
||||
return
|
||||
}
|
||||
SecureLogger.debug("NostrTransport: preparing PM to \(recipientNpub.prefix(16))… id=\(messageID.prefix(8))…", category: .session)
|
||||
guard let embedded = NostrEmbeddedBitChat.encodePMForNostr(content: content, messageID: messageID, recipientPeerID: peerID, senderPeerID: senderPeerID) else {
|
||||
SecureLogger.error("NostrTransport: failed to embed PM packet", category: .session)
|
||||
handlePrivateEnvelopeFailure(
|
||||
events: [],
|
||||
registerPending: false,
|
||||
policy: .userMessage(messageID: messageID)
|
||||
policy: failurePolicy
|
||||
)
|
||||
return
|
||||
}
|
||||
@@ -334,7 +358,7 @@ final class NostrTransport: Transport, @unchecked Sendable {
|
||||
content: embedded,
|
||||
recipientHex: recipientHex,
|
||||
senderIdentity: senderIdentity,
|
||||
failurePolicy: .userMessage(messageID: messageID)
|
||||
failurePolicy: failurePolicy
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -228,9 +228,16 @@ extension ChatViewModel: ChatPrivateConversationContext {
|
||||
NotificationService.shared.sendPrivateMessageNotification(from: senderName, message: message, peerID: peerID)
|
||||
}
|
||||
|
||||
private func makeGeohashNostrTransport() -> NostrTransport {
|
||||
let transport = NostrTransport(keychain: keychain, idBridge: idBridge)
|
||||
func makeGeohashNostrTransport(
|
||||
dependencies: NostrTransport.Dependencies? = nil
|
||||
) -> NostrTransport {
|
||||
let transport = NostrTransport(
|
||||
keychain: keychain,
|
||||
idBridge: idBridge,
|
||||
dependencies: dependencies
|
||||
)
|
||||
transport.senderPeerID = meshService.myPeerID
|
||||
transport.eventDelegate = self
|
||||
return transport
|
||||
}
|
||||
}
|
||||
|
||||
@@ -786,6 +786,63 @@ struct ChatViewModelGeoDMTests {
|
||||
#expect(isFailed(status: viewModel.privateChats[convKey]?.last?.deliveryStatus))
|
||||
}
|
||||
|
||||
@Test @MainActor
|
||||
func geohashTerminalRelayFailure_transitionsSentMessageToFailed() async throws {
|
||||
let (viewModel, _) = makeTestableViewModel()
|
||||
let sender = try NostrIdentity.generate()
|
||||
let recipient = try NostrIdentity.generate()
|
||||
let messageID = "geo-terminal-failure"
|
||||
let convKey = PeerID(nostr_: recipient.publicKeyHex)
|
||||
let message = BitchatMessage(
|
||||
id: messageID,
|
||||
sender: viewModel.nickname,
|
||||
content: "queued geohash message",
|
||||
timestamp: Date(),
|
||||
isRelay: false,
|
||||
isPrivate: true,
|
||||
recipientNickname: "recipient",
|
||||
senderPeerID: viewModel.myPeerID,
|
||||
deliveryStatus: .sent
|
||||
)
|
||||
viewModel.seedPrivateChat([message], for: convKey)
|
||||
|
||||
var terminalFailure: (@MainActor () -> Void)?
|
||||
let dependencies = NostrTransport.Dependencies(
|
||||
notificationCenter: NotificationCenter(),
|
||||
loadFavorites: { [:] },
|
||||
favoriteStatusForNoiseKey: { _ in nil },
|
||||
favoriteStatusForPeerID: { _ in nil },
|
||||
currentIdentity: { sender },
|
||||
registerPendingPrivateEnvelope: { _ in },
|
||||
sendPrivateEnvelopeBatch: { _, failure in
|
||||
terminalFailure = failure
|
||||
return true
|
||||
},
|
||||
scheduleAfter: { _, _ in },
|
||||
relayConnectivity: {
|
||||
Just(false).eraseToAnyPublisher()
|
||||
}
|
||||
)
|
||||
let transport = viewModel.makeGeohashNostrTransport(
|
||||
dependencies: dependencies
|
||||
)
|
||||
|
||||
let accepted = transport.sendPrivateMessageGeohash(
|
||||
content: message.content,
|
||||
toRecipientHex: recipient.publicKeyHex,
|
||||
from: sender,
|
||||
messageID: messageID
|
||||
)
|
||||
|
||||
#expect(accepted)
|
||||
#expect(viewModel.privateChats[convKey]?.first?.deliveryStatus == .sent)
|
||||
let fail = try #require(terminalFailure)
|
||||
fail()
|
||||
#expect(isFailed(
|
||||
status: viewModel.privateChats[convKey]?.first?.deliveryStatus
|
||||
))
|
||||
}
|
||||
|
||||
/// The blocked notice belongs in the DM thread the person is typing in,
|
||||
/// not on the active location-channel timeline.
|
||||
@Test @MainActor
|
||||
|
||||
@@ -424,6 +424,135 @@ struct NostrTransportTests {
|
||||
#expect(publicationProbe.sentEvents.isEmpty)
|
||||
}
|
||||
|
||||
@Test("Direct message with an invalid npub emits a visible failure")
|
||||
@MainActor
|
||||
func invalidDirectRecipientNpubEmitsVisibleFailure() async throws {
|
||||
let keychain = MockKeychain()
|
||||
let idBridge = NostrIdentityBridge(keychain: keychain)
|
||||
let sender = try NostrIdentity.generate()
|
||||
let noiseKey = Data(repeating: 0xA5, count: 32)
|
||||
let peerID = PeerID(hexData: noiseKey)
|
||||
let relationship = makeRelationship(
|
||||
peerNoisePublicKey: noiseKey,
|
||||
peerNostrPublicKey: "not-a-valid-npub",
|
||||
peerNickname: "Invalid npub peer"
|
||||
)
|
||||
let eventProbe = NostrTransportEventProbe()
|
||||
let publicationProbe = NostrTransportProbe()
|
||||
let transport = NostrTransport(
|
||||
keychain: keychain,
|
||||
idBridge: idBridge,
|
||||
dependencies: makeDependencies(
|
||||
favoriteStatusForNoiseKey: {
|
||||
$0 == noiseKey ? relationship : nil
|
||||
},
|
||||
currentIdentity: { sender },
|
||||
sendPrivateEnvelopeBatch: { events, _ in
|
||||
publicationProbe.record(batch: events)
|
||||
}
|
||||
)
|
||||
)
|
||||
transport.senderPeerID = PeerID(str: "0123456789abcdef")
|
||||
transport.eventDelegate = eventProbe
|
||||
|
||||
transport.sendPrivateMessage(
|
||||
"must fail before publication",
|
||||
to: peerID,
|
||||
recipientNickname: "Invalid npub peer",
|
||||
messageID: "invalid-npub"
|
||||
)
|
||||
|
||||
let failed = await TestHelpers.waitUntil(
|
||||
{ eventProbe.failedMessageIDs == ["invalid-npub"] },
|
||||
timeout: 5.0
|
||||
)
|
||||
#expect(failed)
|
||||
#expect(publicationProbe.sentEvents.isEmpty)
|
||||
}
|
||||
|
||||
@Test("Direct message without a resolvable recipient emits a visible failure")
|
||||
@MainActor
|
||||
func missingDirectRecipientEmitsVisibleFailure() async throws {
|
||||
let keychain = MockKeychain()
|
||||
let idBridge = NostrIdentityBridge(keychain: keychain)
|
||||
let sender = try NostrIdentity.generate()
|
||||
let peerID = PeerID(hexData: Data(repeating: 0xC3, count: 32))
|
||||
let eventProbe = NostrTransportEventProbe()
|
||||
let publicationProbe = NostrTransportProbe()
|
||||
let transport = NostrTransport(
|
||||
keychain: keychain,
|
||||
idBridge: idBridge,
|
||||
dependencies: makeDependencies(
|
||||
currentIdentity: { sender },
|
||||
sendPrivateEnvelopeBatch: { events, _ in
|
||||
publicationProbe.record(batch: events)
|
||||
}
|
||||
)
|
||||
)
|
||||
transport.senderPeerID = PeerID(str: "0123456789abcdef")
|
||||
transport.eventDelegate = eventProbe
|
||||
|
||||
transport.sendPrivateMessage(
|
||||
"must fail without recipient",
|
||||
to: peerID,
|
||||
recipientNickname: "Unknown peer",
|
||||
messageID: "missing-recipient"
|
||||
)
|
||||
|
||||
let failed = await TestHelpers.waitUntil(
|
||||
{ eventProbe.failedMessageIDs == ["missing-recipient"] },
|
||||
timeout: 5.0
|
||||
)
|
||||
#expect(failed)
|
||||
#expect(publicationProbe.sentEvents.isEmpty)
|
||||
}
|
||||
|
||||
@Test("Direct message without a current identity emits a visible failure")
|
||||
@MainActor
|
||||
func missingDirectSenderIdentityEmitsVisibleFailure() async throws {
|
||||
let keychain = MockKeychain()
|
||||
let idBridge = NostrIdentityBridge(keychain: keychain)
|
||||
let recipient = try NostrIdentity.generate()
|
||||
let noiseKey = Data(repeating: 0x5A, count: 32)
|
||||
let peerID = PeerID(hexData: noiseKey)
|
||||
let relationship = makeRelationship(
|
||||
peerNoisePublicKey: noiseKey,
|
||||
peerNostrPublicKey: recipient.npub,
|
||||
peerNickname: "Missing identity peer"
|
||||
)
|
||||
let eventProbe = NostrTransportEventProbe()
|
||||
let publicationProbe = NostrTransportProbe()
|
||||
let transport = NostrTransport(
|
||||
keychain: keychain,
|
||||
idBridge: idBridge,
|
||||
dependencies: makeDependencies(
|
||||
favoriteStatusForNoiseKey: {
|
||||
$0 == noiseKey ? relationship : nil
|
||||
},
|
||||
currentIdentity: { nil },
|
||||
sendPrivateEnvelopeBatch: { events, _ in
|
||||
publicationProbe.record(batch: events)
|
||||
}
|
||||
)
|
||||
)
|
||||
transport.senderPeerID = PeerID(str: "0123456789abcdef")
|
||||
transport.eventDelegate = eventProbe
|
||||
|
||||
transport.sendPrivateMessage(
|
||||
"must fail without identity",
|
||||
to: peerID,
|
||||
recipientNickname: "Missing identity peer",
|
||||
messageID: "missing-identity"
|
||||
)
|
||||
|
||||
let failed = await TestHelpers.waitUntil(
|
||||
{ eventProbe.failedMessageIDs == ["missing-identity"] },
|
||||
timeout: 5.0
|
||||
)
|
||||
#expect(failed)
|
||||
#expect(publicationProbe.sentEvents.isEmpty)
|
||||
}
|
||||
|
||||
@Test("Rejected favorite notification retains and retries the exact pair")
|
||||
@MainActor
|
||||
func rejectedFavoriteNotificationRetriesExactPair() async throws {
|
||||
|
||||
Reference in New Issue
Block a user