diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Hex.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Hex.kt index d9f70e24cd..f92e5830de 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Hex.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Hex.kt @@ -47,4 +47,37 @@ fun decodePublicKey(key: String): ByteArray { //} else { Hex.decode(key) } +} + +data class DirtyKeyInfo(val type: String, val keyHex: String, val restOfWord: String) + +fun parseDirtyWordForKey(mightBeAKey: String): DirtyKeyInfo? { + var key = mightBeAKey + if (key.startsWith("nostr:", true)) { + key = key.substring("nostr:".length) + } + + key = key.removePrefix("@") + + if (key.length < 63) + return null + + try { + + val keyB32 = key.substring(0, 63) + val restOfWord = key.substring(63) + + if (key.startsWith("nsec1", true)) { + return DirtyKeyInfo("npub", Persona(privKey = keyB32.bechToBytes()).pubKey.toHexKey(), restOfWord) + } else if (key.startsWith("npub1", true)) { + return DirtyKeyInfo("npub", keyB32.bechToBytes().toHexKey(), restOfWord) + } else if (key.startsWith("note1", true)) { + return DirtyKeyInfo("note", keyB32.bechToBytes().toHexKey(), restOfWord) + } + + } catch (e: Exception) { + e.printStackTrace() + } + + return null } \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index 914058f183..f34b3fc670 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -74,6 +74,16 @@ object LocalCache { } } + fun checkGetOrCreateNote(key: HexKey): Note? { + return try { + val checkHex = Hex.decode(key) // Checks if this is a valid Hex + getOrCreateNote(key) + } catch (e: IllegalArgumentException) { + Log.e("LocalCache", "Invalid Key to create note: $key", e) + null + } + } + @Synchronized fun getOrCreateNote(idHex: String): Note { return notes[idHex] ?: run { @@ -83,6 +93,17 @@ object LocalCache { } } + fun checkGetOrCreateChannel(key: HexKey): Channel? { + return try { + val checkHex = Hex.decode(key) // Checks if this is a valid Hex + getOrCreateChannel(key) + } catch (e: IllegalArgumentException) { + Log.e("LocalCache", "Invalid Key to create channel: $key", e) + null + } + } + + @Synchronized fun getOrCreateChannel(key: String): Channel { return channels[key] ?: run { @@ -145,7 +166,6 @@ object LocalCache { if (isRepeatedMessageSpam(event)) return val note = getOrCreateNote(event.id.toHex()) - val author = getOrCreateUser(event.pubKey.toHexKey()) if (relay != null) { @@ -157,7 +177,7 @@ object LocalCache { if (note.event != null) return val mentions = event.mentions.mapNotNull { checkGetOrCreateUser(it) } - val replyTo = replyToWithoutCitations(event).map { getOrCreateNote(it) } + val replyTo = replyToWithoutCitations(event).mapNotNull { checkGetOrCreateNote(it) } note.loadEvent(event, author, mentions, replyTo) @@ -266,7 +286,7 @@ object LocalCache { //Log.d("PM", "${author.toBestDisplayName()} to ${recipient?.toBestDisplayName()}") - val repliesTo = event.tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }.map { getOrCreateNote(it) } + val repliesTo = event.tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }.mapNotNull { checkGetOrCreateNote(it) } val mentions = event.tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }.mapNotNull { checkGetOrCreateUser(it) } note.loadEvent(event, author, mentions, repliesTo) @@ -293,7 +313,7 @@ object LocalCache { val author = getOrCreateUser(event.pubKey.toHexKey()) val mentions = event.originalAuthor.mapNotNull { checkGetOrCreateUser(it) } - val repliesTo = event.boostedPost.map { getOrCreateNote(it) } + val repliesTo = event.boostedPost.mapNotNull { checkGetOrCreateNote(it) } note.loadEvent(event, author, mentions, repliesTo) @@ -324,7 +344,7 @@ object LocalCache { val author = getOrCreateUser(event.pubKey.toHexKey()) val mentions = event.originalAuthor.mapNotNull { checkGetOrCreateUser(it) } - val repliesTo = event.originalPost.map { getOrCreateNote(it) } + val repliesTo = event.originalPost.mapNotNull { checkGetOrCreateNote(it) } note.loadEvent(event, author, mentions, repliesTo) @@ -369,7 +389,7 @@ object LocalCache { val author = getOrCreateUser(event.pubKey.toHexKey()) val mentions = event.reportedAuthor.mapNotNull { checkGetOrCreateUser(it) } - val repliesTo = event.reportedPost.map { getOrCreateNote(it) } + val repliesTo = event.reportedPost.mapNotNull { checkGetOrCreateNote(it) } note.loadEvent(event, author, mentions, repliesTo) @@ -410,7 +430,7 @@ object LocalCache { if (event.channel.isNullOrBlank()) return // new event - val oldChannel = getOrCreateChannel(event.channel) + val oldChannel = checkGetOrCreateChannel(event.channel) ?: return val author = getOrCreateUser(event.pubKey.toHexKey()) if (event.createdAt > oldChannel.updatedMetadataAt) { if (oldChannel.creator == null || oldChannel.creator == author) { @@ -432,7 +452,7 @@ object LocalCache { if (event.channel.isNullOrBlank()) return if (isRepeatedMessageSpam(event)) return - val channel = getOrCreateChannel(event.channel) + val channel = checkGetOrCreateChannel(event.channel) ?: return val note = getOrCreateNote(event.id.toHex()) channel.addNote(note) @@ -449,7 +469,7 @@ object LocalCache { val mentions = event.mentions.mapNotNull { checkGetOrCreateUser(it) } val replyTo = event.replyTos - .map { getOrCreateNote(it) } + .mapNotNull { checkGetOrCreateNote(it) } .filter { it.event !is ChannelCreateEvent } note.channel = channel @@ -489,7 +509,7 @@ object LocalCache { val author = getOrCreateUser(event.pubKey.toHexKey()) val mentions = event.zappedAuthor.mapNotNull { checkGetOrCreateUser(it) } - val repliesTo = event.zappedPost.map { getOrCreateNote(it) } + val repliesTo = event.zappedPost.mapNotNull { checkGetOrCreateNote(it) } note.loadEvent(event, author, mentions, repliesTo) @@ -525,7 +545,7 @@ object LocalCache { val author = getOrCreateUser(event.pubKey.toHexKey()) val mentions = event.zappedAuthor.mapNotNull { checkGetOrCreateUser(it) } - val repliesTo = event.zappedPost.map { getOrCreateNote(it) } + val repliesTo = event.zappedPost.mapNotNull { checkGetOrCreateNote(it) } note.loadEvent(event, author, mentions, repliesTo) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/ThreadAssembler.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/ThreadAssembler.kt index afec5add10..5b930415d6 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/ThreadAssembler.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/ThreadAssembler.kt @@ -9,7 +9,7 @@ class ThreadAssembler { if (note.replyTo == null || note.replyTo?.isEmpty() == true) return note val markedAsRoot = note.event?.tags?.firstOrNull { it[0] == "e" && it.size > 3 && it[3] == "root" }?.getOrNull(1) - if (markedAsRoot != null) return LocalCache.getOrCreateNote(markedAsRoot) + if (markedAsRoot != null) return LocalCache.checkGetOrCreateNote(markedAsRoot) val hasNoReplyTo = note.replyTo?.firstOrNull { it.replyTo?.isEmpty() == true } if (hasNoReplyTo != null) return hasNoReplyTo diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt index bdf7ac785a..b43b7fc389 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt @@ -76,24 +76,12 @@ class NewPostViewModel: ViewModel() { // adds all references to mentions and reply tos message.text.split('\n').forEach { paragraph: String -> paragraph.split(' ').forEach { word: String -> - try { - if (word.startsWith("@npub") && word.length >= 64) { - val keyB32 = word.substring(0, 64) + val results = parseDirtyWordForKey(word) - val key = decodePublicKey(keyB32.removePrefix("@")) - val user = LocalCache.getOrCreateUser(key.toHexKey()) - - addUserToMentions(user) - } else if (word.startsWith("@note") && word.length >= 64) { - val keyB32 = word.substring(0, 64) - - val key = decodePublicKey(keyB32.removePrefix("@")) - val note = LocalCache.getOrCreateNote(key.toHexKey()) - - addNoteToReplyTos(note) - } - } catch (e: Exception) { - e.printStackTrace() + if (results?.type == "npub") { + addUserToMentions(LocalCache.getOrCreateUser(results.keyHex)) + } else if (results?.type == "note") { + addNoteToReplyTos(LocalCache.getOrCreateNote(results.keyHex)) } } } @@ -101,29 +89,16 @@ class NewPostViewModel: ViewModel() { // Tags the text in the correct order. val newMessage = message.text.split('\n').map { paragraph: String -> paragraph.split(' ').map { word: String -> - try { - if (word.startsWith("@npub") && word.length >= 64) { - val keyB32 = word.substring(0, 64) - val restOfWord = word.substring(64) + val results = parseDirtyWordForKey(word) + if (results?.type == "npub") { + val user = LocalCache.getOrCreateUser(results.keyHex) - val key = decodePublicKey(keyB32.removePrefix("@")) - val user = LocalCache.getOrCreateUser(key.toHexKey()) + "#[${tagIndex(user)}]${results.restOfWord}" + } else if (results?.type == "note") { + val note = LocalCache.getOrCreateNote(results.keyHex) - "#[${tagIndex(user)}]$restOfWord" - } else if (word.startsWith("@note") && word.length >= 64) { - val keyB32 = word.substring(0, 64) - val restOfWord = word.substring(64) - - val key = decodePublicKey(keyB32.removePrefix("@")) - val note = LocalCache.getOrCreateNote(key.toHexKey()) - - "#[${tagIndex(note)}]$restOfWord" - } else { - word - } - } catch (e: Exception) { - e.printStackTrace() - // if it can't parse the key, don't try to change. + "#[${tagIndex(note)}]${results.restOfWord}" + } else { word } }.joinToString(" ") diff --git a/app/src/test/java/com/vitorpamplona/amethyst/ExampleUnitTest.kt b/app/src/test/java/com/vitorpamplona/amethyst/ExampleUnitTest.kt deleted file mode 100644 index 7287da1693..0000000000 --- a/app/src/test/java/com/vitorpamplona/amethyst/ExampleUnitTest.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.vitorpamplona.amethyst - -import org.junit.Assert.assertEquals -import org.junit.Test - -/** - * Example local unit test, which will execute on the development machine (host). - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -class ExampleUnitTest { - @Test - fun addition_isCorrect() { - assertEquals(4, 2 + 2) - } -} \ No newline at end of file diff --git a/app/src/test/java/com/vitorpamplona/amethyst/KeyParseTest.kt b/app/src/test/java/com/vitorpamplona/amethyst/KeyParseTest.kt new file mode 100644 index 0000000000..f9053cabf8 --- /dev/null +++ b/app/src/test/java/com/vitorpamplona/amethyst/KeyParseTest.kt @@ -0,0 +1,93 @@ +package com.vitorpamplona.amethyst + +import com.vitorpamplona.amethyst.model.parseDirtyWordForKey +import org.junit.Assert.assertEquals +import org.junit.Test + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class KeyParseTest { + @Test + fun keyParseTestNote() { + val result = parseDirtyWordForKey("note1z5e2m0smx6d7e2d0zaq8d3rnd7httm6j0uf8tf90yqqjrs842czshwtkmn") + assertEquals("note", result?.type) + assertEquals("1532adbe1b369beca9af174076c4736faeb5ef527f1275a4af200121c0f55605", result?.keyHex) + assertEquals("", result?.restOfWord) + } + + @Test + fun keyParseTestPub() { + val result = parseDirtyWordForKey("npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z") + assertEquals("npub", result?.type) + assertEquals("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c", result?.keyHex) + assertEquals("", result?.restOfWord) + } + + @Test + fun keyParseTestNoteWithExtraChars() { + val result = parseDirtyWordForKey("note1z5e2m0smx6d7e2d0zaq8d3rnd7httm6j0uf8tf90yqqjrs842czshwtkmn,") + assertEquals("note", result?.type) + assertEquals("1532adbe1b369beca9af174076c4736faeb5ef527f1275a4af200121c0f55605", result?.keyHex) + assertEquals(",", result?.restOfWord) + } + + @Test + fun keyParseTestPubWithExtraChars() { + val result = parseDirtyWordForKey("npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z,") + assertEquals("npub", result?.type) + assertEquals("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c", result?.keyHex) + assertEquals(",", result?.restOfWord) + } + + @Test + fun keyParseTestNoteWithExtraCharsAndAt() { + val result = parseDirtyWordForKey("@note1z5e2m0smx6d7e2d0zaq8d3rnd7httm6j0uf8tf90yqqjrs842czshwtkmn,") + assertEquals("note", result?.type) + assertEquals("1532adbe1b369beca9af174076c4736faeb5ef527f1275a4af200121c0f55605", result?.keyHex) + assertEquals(",", result?.restOfWord) + } + + @Test + fun keyParseTestPubWithExtraCharsAndAt() { + val result = parseDirtyWordForKey("@npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z,") + assertEquals("npub", result?.type) + assertEquals("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c", result?.keyHex) + assertEquals(",", result?.restOfWord) + } + + @Test + fun keyParseTestNoteWithExtraCharsAndNostrPrefix() { + val result = parseDirtyWordForKey("nostr:note1z5e2m0smx6d7e2d0zaq8d3rnd7httm6j0uf8tf90yqqjrs842czshwtkmn,") + assertEquals("note", result?.type) + assertEquals("1532adbe1b369beca9af174076c4736faeb5ef527f1275a4af200121c0f55605", result?.keyHex) + assertEquals(",", result?.restOfWord) + } + + @Test + fun keyParseTestPubWithExtraCharsAndNostrPrefix() { + val result = parseDirtyWordForKey("nostr:npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z,") + assertEquals("npub", result?.type) + assertEquals("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c", result?.keyHex) + assertEquals(",", result?.restOfWord) + } + + + @Test + fun keyParseTestUppercaseNoteWithExtraCharsAndNostrPrefix() { + val result = parseDirtyWordForKey("Nostr:note1z5e2m0smx6d7e2d0zaq8d3rnd7httm6j0uf8tf90yqqjrs842czshwtkmn,") + assertEquals("note", result?.type) + assertEquals("1532adbe1b369beca9af174076c4736faeb5ef527f1275a4af200121c0f55605", result?.keyHex) + assertEquals(",", result?.restOfWord) + } + + @Test + fun keyParseTestUppercasePubWithExtraCharsAndNostrPrefix() { + val result = parseDirtyWordForKey("nOstr:npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z,") + assertEquals("npub", result?.type) + assertEquals("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c", result?.keyHex) + assertEquals(",", result?.restOfWord) + } +} \ No newline at end of file