Skip to content

Commit

Permalink
fixed a bug where nostr entities in URLs were parsed like quoted note…
Browse files Browse the repository at this point in the history
… links #1429
  • Loading branch information
bryanmontz committed Sep 2, 2024
1 parent 404d12a commit 675fd3c
Show file tree
Hide file tree
Showing 5 changed files with 89 additions and 13 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Removed the like and repost counts from the Main and Profile feeds.
- Removed wss:// from relay addresses in lists and removed the need to prepend relay addresses with wss://.
- Localized the quotation marks on the Notifications view.
- Fixed a bug where nostr entities in URLs were treated like quoted note links.

### Internal Changes
- Included the npub in the properties list sent to analytics.
Expand Down
6 changes: 6 additions & 0 deletions Nos.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,8 @@
5095330B2C625B5D00E0BACA /* zap_request_one_sat.json in Resources */ = {isa = PBXBuildFile; fileRef = 509533092C625B5D00E0BACA /* zap_request_one_sat.json */; };
5095330C2C625B5D00E0BACA /* zap_request_no_amount.json in Resources */ = {isa = PBXBuildFile; fileRef = 5095330A2C625B5D00E0BACA /* zap_request_no_amount.json */; };
50DE6B1B2C6B88FE0065665D /* View+StyledBorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50DE6B1A2C6B88FE0065665D /* View+StyledBorder.swift */; };
50E2EB722C86175900D4B360 /* NSRegularExpression+Replacement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50E2EB712C86175900D4B360 /* NSRegularExpression+Replacement.swift */; };
50E2EB7B2C8617C800D4B360 /* NSRegularExpression+Replacement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50E2EB712C86175900D4B360 /* NSRegularExpression+Replacement.swift */; };
50F695072C6392C4000E4C74 /* zap_receipt.json in Resources */ = {isa = PBXBuildFile; fileRef = 50F695062C6392C4000E4C74 /* zap_receipt.json */; };
5B098DBC2BDAF6CB00500A1B /* NoteParserTests+NIP08.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B098DBB2BDAF6CB00500A1B /* NoteParserTests+NIP08.swift */; };
5B098DC62BDAF73500500A1B /* AttributedString+Links.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B098DC52BDAF73500500A1B /* AttributedString+Links.swift */; };
Expand Down Expand Up @@ -624,6 +626,7 @@
509533092C625B5D00E0BACA /* zap_request_one_sat.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = zap_request_one_sat.json; sourceTree = "<group>"; };
5095330A2C625B5D00E0BACA /* zap_request_no_amount.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = zap_request_no_amount.json; sourceTree = "<group>"; };
50DE6B1A2C6B88FE0065665D /* View+StyledBorder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+StyledBorder.swift"; sourceTree = "<group>"; };
50E2EB712C86175900D4B360 /* NSRegularExpression+Replacement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSRegularExpression+Replacement.swift"; sourceTree = "<group>"; };
50F695062C6392C4000E4C74 /* zap_receipt.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = zap_receipt.json; sourceTree = "<group>"; };
5B098DBB2BDAF6CB00500A1B /* NoteParserTests+NIP08.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NoteParserTests+NIP08.swift"; sourceTree = "<group>"; };
5B098DC52BDAF73500500A1B /* AttributedString+Links.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AttributedString+Links.swift"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1494,6 +1497,7 @@
C9ADB14029951CB10075E7F8 /* NSManagedObject+Nos.swift */,
C97A1C8D29E58EC7009D9E8D /* NSManagedObjectContext+Nos.swift */,
C93EC2F329C34C860012EE2A /* NSPredicate+Bool.swift */,
50E2EB712C86175900D4B360 /* NSRegularExpression+Replacement.swift */,
C93EC2F629C351470012EE2A /* Optional+Unwrap.swift */,
C99721CA2AEBED26004EBEAB /* String+Empty.swift */,
C9ADB13729928CC30075E7F8 /* String+Hex.swift */,
Expand Down Expand Up @@ -2014,6 +2018,7 @@
C98B8B4029FBF83B009789C8 /* NotificationCard.swift in Sources */,
5B834F672A83FB5C000C1432 /* ProfileKnownFollowersView.swift in Sources */,
C9E8C1152B081EBE002D46B0 /* NIP05View.swift in Sources */,
50E2EB722C86175900D4B360 /* NSRegularExpression+Replacement.swift in Sources */,
C92E7F6A2C4EFF7200B80638 /* WebSocketConnection.swift in Sources */,
5BC0D9CC2B867B9D005D6980 /* NamesAPI.swift in Sources */,
C987F81D29BA6D9A00B44E7A /* ProfileTab.swift in Sources */,
Expand Down Expand Up @@ -2379,6 +2384,7 @@
03B4E6AF2C125D61006E5F59 /* FileStorageUploadResponseJSON.swift in Sources */,
A3B943D8299D758F00A15A08 /* Keychain.swift in Sources */,
035729B92BE416A6005FEE85 /* GiftWrapperTests.swift in Sources */,
50E2EB7B2C8617C800D4B360 /* NSRegularExpression+Replacement.swift in Sources */,
032634702C10C40B00E489B5 /* NostrBuildAPIClientTests.swift in Sources */,
0315B5F02C7E451C0020E707 /* MockMediaService.swift in Sources */,
C9646EAA29B7A506007239A4 /* Analytics.swift in Sources */,
Expand Down
31 changes: 31 additions & 0 deletions Nos/Extensions/NSRegularExpression+Replacement.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import Foundation

extension NSRegularExpression {

/// Helper function to perform replacements using NSRegularExpression.
/// - Parameters:
/// - string: The input string.
/// - options: Matching options to use.
/// - range: A range in which to perform replacements.
/// - transform: A transformation function to perform on the match before replacement.
/// - Returns: The input string with matches replaced.
func stringByReplacingMatches(
in string: String,
options: NSRegularExpression.MatchingOptions = [],
range: NSRange,
transform: (NSTextCheckingResult) -> String
) -> String {
var result = ""
var lastRangeEnd = string.startIndex

for match in matches(in: string, options: options, range: range) {
guard let matchRange = Range(match.range, in: string) else { continue }
result += string[lastRangeEnd..<matchRange.lowerBound]
result += transform(match)
lastRangeEnd = matchRange.upperBound
}

result += string[lastRangeEnd..<string.endIndex]
return result
}
}
38 changes: 25 additions & 13 deletions Nos/Models/NoteParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -127,25 +127,36 @@ struct NoteParser {
/// Defaults to `false`.
/// - Returns: A tuple of the edited content and the first quoted note id, if it was requested and it exists.
private func replaceNostrEntities(in content: String, capturesFirstNote: Bool = false) -> (String, RawEventID?) {
let unformattedRegex =
/@?(?:nostr:)?(?<entity>((npub1|note1|nprofile1|nevent1|naddr1)[a-zA-Z0-9]{58,}))/
// Note: This pattern contains a lookbehind, which is not currently supported by the newer Swift regex syntax.
let pattern = "(?<=^|\\s|[^:\\/])@?(?:nostr:)?((npub1|note1|nprofile1|nevent1|naddr1)[a-zA-Z0-9]{58,})"
let regex = try! NSRegularExpression(pattern: pattern, options: []) // swiftlint:disable:this force_try

var firstNoteID: RawEventID?
let result = content.replacing(unformattedRegex) { match in
let substring = match.0
let entity = match.1

let result = regex.stringByReplacingMatches(
in: content,
options: [],
range: NSRange(location: 0, length: content.utf16.count)
) { match in
let nsRange = match.range(at: 0)
guard let range = Range(nsRange, in: content) else { return "" }
let substring = String(content[range])

let entityRange = match.range(at: 1)
guard let entityRange = Range(entityRange, in: content) else { return substring }
let entity = String(content[entityRange])

var prefix = ""
let firstCharacter = String(String(substring).prefix(1))
let firstCharacter = String(substring.prefix(1))
if firstCharacter.range(of: #"\s|\r\n|\r|\n"#, options: .regularExpression) != nil {
prefix = firstCharacter
}
let string = String(entity)


do {
let identifier = try NostrIdentifier.decode(bech32String: string)
let identifier = try NostrIdentifier.decode(bech32String: entity)
switch identifier {
case .npub(let rawAuthorID), .nprofile(let rawAuthorID, _):
return "\(prefix)[\(string)](@\(rawAuthorID))"
return "\(prefix)[\(entity)](@\(rawAuthorID))"
case .note(let rawEventID), .nevent(let rawEventID, _, _, _):
if capturesFirstNote && firstNoteID == nil {
firstNoteID = rawEventID
Expand All @@ -155,14 +166,15 @@ struct NoteParser {
}
case .naddr(let replaceableID, _, let authorID, let kind):
return "\(prefix)[\(String(localized: .localizable.linkToNote))]" +
"($\(replaceableID);\(authorID);\(kind))"
"($\(replaceableID);\(authorID);\(kind))"
case .nsec:
return String(substring)
return substring
}
} catch {
return String(substring)
return substring
}
}

return (result, firstNoteID)
}

Expand Down
26 changes: 26 additions & 0 deletions NosTests/Models/NoteParserTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -299,4 +299,30 @@ final class NoteParserTests: CoreDataTestCase {
XCTAssertEqual(links[safe: 0]?.key, "🔗 Link to note")
XCTAssertEqual(links[safe: 0]?.value, URL(string: naddrLink))
}

@MainActor func testContentWithYakihonneNeventLink() {
// swiftlint:disable line_length
let content = """
"https://yakihonne.com/notes/nevent1qgszpxr0hql8whvk6xyv5hya7yxwd4snur4hu4mg5rctz2ehekkzrvcqyrej80hs0k7ydd60p4zpdddqlx4zr66fwns5frwn2zf2gg3u8vr3w725fc0"
"""

let expectedContent = "\"yakihonne.com...\""
// swiftlint:enable line_length

let components = sut.components(
from: content,
tags: [[]],
context: testContext
)
let attributedContent = components.attributedContent

let parsedContent = String(attributedContent.characters)
XCTAssertEqual(parsedContent, expectedContent)

let links = attributedContent.links
XCTAssertEqual(links.count, 1)
XCTAssertEqual(links[safe: 0]?.key, "yakihonne.com...")

XCTAssertNil(components.quotedNoteID)
}
}

0 comments on commit 675fd3c

Please sign in to comment.