mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-24 22:45:19 +00:00
Fix Cashu long-message guard bypass
This commit is contained in:
@@ -255,9 +255,8 @@ final class MessageFormattingEngine {
|
||||
isSelf: Bool,
|
||||
isMentioned: Bool
|
||||
) -> AttributedString {
|
||||
// For very long content without special tokens, use plain formatting
|
||||
let containsCashu = containsCashuToken(content)
|
||||
if (content.count > 4000 || content.hasVeryLongToken(threshold: 1024)) && !containsCashu {
|
||||
// For very long content, use plain formatting to avoid expensive regex and detector work.
|
||||
if content.isOversizedForRichFormatting() {
|
||||
return formatPlainContent(content, baseColor: baseColor, isSelf: isSelf)
|
||||
}
|
||||
|
||||
|
||||
@@ -70,12 +70,7 @@ final class ChatMessageFormatter {
|
||||
let content = message.content
|
||||
let nsContent = content as NSString
|
||||
let nsLen = nsContent.length
|
||||
let containsCashuEarly: Bool = {
|
||||
let regex = Patterns.quickCashuPresence
|
||||
return regex.numberOfMatches(in: content, options: [], range: NSRange(location: 0, length: nsLen)) > 0
|
||||
}()
|
||||
|
||||
if (content.count > 4000 || content.hasVeryLongToken(threshold: 1024)) && !containsCashuEarly {
|
||||
if content.isOversizedForRichFormatting() {
|
||||
var plainStyle = AttributeContainer()
|
||||
plainStyle.foregroundColor = baseColor
|
||||
plainStyle.font = isSelf
|
||||
|
||||
@@ -22,7 +22,7 @@ struct TextMessageView: View {
|
||||
let cashuLinks = message.content.extractCashuLinks()
|
||||
let lightningLinks = message.content.extractLightningLinks()
|
||||
HStack(alignment: .top, spacing: 0) {
|
||||
let isLong = (message.content.count > TransportConfig.uiLongMessageLengthThreshold || message.content.hasVeryLongToken(threshold: TransportConfig.uiVeryLongTokenThreshold)) && cashuLinks.isEmpty
|
||||
let isLong = message.content.isLongForDisplay()
|
||||
let isExpanded = expandedMessageIDs.contains(message.id)
|
||||
Text(conversationUIModel.formatMessage(message, colorScheme: colorScheme))
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
@@ -38,7 +38,7 @@ struct TextMessageView: View {
|
||||
}
|
||||
|
||||
// Expand/Collapse for very long messages
|
||||
if (message.content.count > TransportConfig.uiLongMessageLengthThreshold || message.content.hasVeryLongToken(threshold: TransportConfig.uiVeryLongTokenThreshold)) && cashuLinks.isEmpty {
|
||||
if message.content.isLongForDisplay() {
|
||||
let isExpanded = expandedMessageIDs.contains(message.id)
|
||||
let labelKey = isExpanded ? LocalizedStringKey("content.message.show_less") : LocalizedStringKey("content.message.show_more")
|
||||
Button(labelKey) {
|
||||
|
||||
@@ -21,6 +21,19 @@ extension String {
|
||||
return current >= threshold
|
||||
}
|
||||
|
||||
// Detect if message content should be collapsed to avoid expensive unbounded layout.
|
||||
func isLongForDisplay(
|
||||
lengthThreshold: Int = TransportConfig.uiLongMessageLengthThreshold,
|
||||
tokenThreshold: Int = TransportConfig.uiVeryLongTokenThreshold
|
||||
) -> Bool {
|
||||
count > lengthThreshold || hasVeryLongToken(threshold: tokenThreshold)
|
||||
}
|
||||
|
||||
// Detect if message content should use plain formatting to avoid expensive regex parsing.
|
||||
func isOversizedForRichFormatting(lengthThreshold: Int = 4000, tokenThreshold: Int = 1024) -> Bool {
|
||||
count > lengthThreshold || hasVeryLongToken(threshold: tokenThreshold)
|
||||
}
|
||||
|
||||
// Extract up to `max` Cashu tokens (cashuA/cashuB). Allow dot '.' and shorter lengths.
|
||||
func extractCashuLinks(max: Int = 3) -> [String] {
|
||||
let regex = MessageFormattingEngine.Patterns.cashu
|
||||
|
||||
@@ -606,6 +606,25 @@ struct ChatViewModelFormattingTests {
|
||||
|
||||
#expect(String(header.characters) == "<@Alice#a1b2> ")
|
||||
}
|
||||
|
||||
@Test @MainActor
|
||||
func formatMessageAsText_longCashuMessageUsesPlainFastPath() async {
|
||||
let (viewModel, _) = makeTestableViewModel()
|
||||
let cashu = "cashuA" + String(repeating: "a", count: 40)
|
||||
let longContent = "hi @bob " + cashu + " " + String(repeating: "x", count: 4_100)
|
||||
let message = BitchatMessage(
|
||||
id: "fmt-long-cashu",
|
||||
sender: "Alice#a1b2",
|
||||
content: longContent,
|
||||
timestamp: Date(timeIntervalSince1970: 1_700_010_123),
|
||||
isRelay: false,
|
||||
senderPeerID: PeerID(str: "00000000000000b3")
|
||||
)
|
||||
|
||||
let formatted = viewModel.formatMessageAsText(message, colorScheme: .light)
|
||||
|
||||
#expect(String(formatted.characters) == "<@Alice#a1b2> \(longContent) [\(message.formattedTimestamp)]")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Verification Tests
|
||||
|
||||
@@ -323,6 +323,32 @@ struct MessageFormattingEngineTests {
|
||||
// Exactly at threshold DOES trigger (uses >= comparison)
|
||||
#expect(content.hasVeryLongToken(threshold: 50))
|
||||
}
|
||||
|
||||
@Test func isLongForDisplay_doesNotIgnoreCashuLinks() {
|
||||
let cashu = "cashuA" + String(repeating: "a", count: 40)
|
||||
let content = String(repeating: "a", count: TransportConfig.uiLongMessageLengthThreshold + 1) + " " + cashu
|
||||
|
||||
#expect(content.extractCashuLinks().count == 1)
|
||||
#expect(content.isLongForDisplay())
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@Test func formatMessage_longCashuMessageFallsBackToPlainContentPath() {
|
||||
let context = MockMessageFormattingContext(nickname: "carol")
|
||||
let cashu = "cashuA" + String(repeating: "a", count: 40)
|
||||
let longContent = "hi @bob " + cashu + " " + String(repeating: "x", count: 4_100)
|
||||
let message = BitchatMessage(
|
||||
id: "long-cashu",
|
||||
sender: "alice",
|
||||
content: longContent,
|
||||
timestamp: Date(timeIntervalSince1970: 1_700_000_999),
|
||||
isRelay: false
|
||||
)
|
||||
|
||||
let formatted = MessageFormattingEngine.formatMessage(message, context: context, colorScheme: .light)
|
||||
|
||||
#expect(String(formatted.characters) == "<@alice> \(longContent) [\(message.formattedTimestamp)]")
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
|
||||
Reference in New Issue
Block a user