From 502d39c89325ad28bb800405f7e582583df709ca Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 14 Jan 2025 16:59:53 -0500 Subject: [PATCH] Generalizes the hashtag and index tag parser for the content as well as the way to pass params to build them on create. --- .../vitorpamplona/amethyst/model/Account.kt | 1 - .../loggedIn/search/SearchBarViewModel.kt | 2 +- .../InteractiveStoryBaseEvent.kt | 17 ++- .../quartz/nip01Core/tags/geohash/GeoHash.kt | 4 +- .../nip01Core/tags/hashtags/HashTags.kt | 37 ++++++ .../quartz/nip10Notes/BaseTextNoteEvent.kt | 107 ++++-------------- .../vitorpamplona/quartz/nip10Notes/ETag.kt | 20 ---- .../quartz/nip10Notes/TextNoteEvent.kt | 19 +--- .../nip10Notes/content/ContentHashTags.kt | 42 +++++++ .../quartz/nip10Notes/content/IndexedTags.kt | 72 ++++++++++++ .../quartz/nip10Notes/content/Urls.kt | 35 ++++++ .../quartz/nip19Bech32/Nip19Parser.kt | 19 ++++ .../quartz/nip22Comments/CommentEvent.kt | 17 +-- .../quartz/nip34Git/GitReplyEvent.kt | 15 +-- .../nip35Torrents/TorrentCommentEvent.kt | 20 ++-- .../quartz/nip68Picture/PictureEvent.kt | 17 +-- .../nip99Classifieds/ClassifiedsEvent.kt | 11 +- 17 files changed, 272 insertions(+), 183 deletions(-) create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/hashtags/HashTags.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/content/ContentHashTags.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/content/IndexedTags.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/content/Urls.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 91d9b91db..f6bd8067a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -2275,7 +2275,6 @@ class Account( replyTos = repliesToHex, mentions = mentionsHex, addresses = addresses, - extraTags = null, zapReceiver = zapReceiver, markAsSensitive = wantsToMarkAsSensitive, zapRaiserAmount = zapRaiserAmount, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchBarViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchBarViewModel.kt index a8eb8fe91..160659ce6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchBarViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchBarViewModel.kt @@ -35,7 +35,7 @@ import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.ammolite.relays.BundledUpdate -import com.vitorpamplona.quartz.nip10Notes.findHashtags +import com.vitorpamplona.quartz.nip10Notes.content.findHashtags import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/interactiveStories/InteractiveStoryBaseEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/interactiveStories/InteractiveStoryBaseEvent.kt index 8bc70d40c..8c8e47160 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/interactiveStories/InteractiveStoryBaseEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/interactiveStories/InteractiveStoryBaseEvent.kt @@ -25,8 +25,10 @@ import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent import com.vitorpamplona.quartz.nip01Core.core.firstTagValue import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip01Core.tags.geohash.geohashMipMap -import com.vitorpamplona.quartz.nip10Notes.findHashtags -import com.vitorpamplona.quartz.nip10Notes.findURLs +import com.vitorpamplona.quartz.nip01Core.tags.hashtags.buildHashtagTags +import com.vitorpamplona.quartz.nip10Notes.content.buildUrlRefs +import com.vitorpamplona.quartz.nip10Notes.content.findHashtags +import com.vitorpamplona.quartz.nip10Notes.content.findURLs import com.vitorpamplona.quartz.nip19Bech32.parse import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrl import com.vitorpamplona.quartz.nip57Zaps.ZapSplitSetup @@ -64,14 +66,9 @@ open class InteractiveStoryBaseEvent( emojis: List? = null, ): Array> { val tags = mutableListOf>() - findHashtags(content).forEach { - val lowercaseTag = it.lowercase() - tags.add(arrayOf("t", it)) - if (it != lowercaseTag) { - tags.add(arrayOf("t", it.lowercase())) - } - } - findURLs(content).forEach { tags.add(arrayOf("r", it)) } + + tags.addAll(buildHashtagTags(findHashtags(content))) + tags.addAll(buildUrlRefs(findURLs(content))) zapReceiver?.forEach { tags.add(arrayOf("zap", it.lnAddressOrPubKeyHex, it.relay ?: "", it.weight.toString())) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/geohash/GeoHash.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/geohash/GeoHash.kt index 4d4364d81..d4006c78a 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/geohash/GeoHash.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/geohash/GeoHash.kt @@ -20,7 +20,9 @@ */ package com.vitorpamplona.quartz.nip01Core.tags.geohash -fun geohashMipMap(geohash: String): Array> = +import com.vitorpamplona.quartz.nip01Core.core.TagArray + +fun geohashMipMap(geohash: String): TagArray = geohash.indices .asSequence() .map { arrayOf("g", geohash.substring(0, it + 1)) } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/hashtags/HashTags.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/hashtags/HashTags.kt new file mode 100644 index 000000000..f2c124274 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/hashtags/HashTags.kt @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.tags.hashtags + +fun buildHashtagTags(tags: List): List> { + val uniqueTags = mutableSetOf() + + tags.forEach { tag -> + uniqueTags.add(tag) + val lowercaseTag = tag.lowercase() + if (tag != lowercaseTag) { + uniqueTags.add(lowercaseTag) + } + } + + return uniqueTags.map { + arrayOf("t", it) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/BaseTextNoteEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/BaseTextNoteEvent.kt index ad6fc545a..65a3b0bbe 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/BaseTextNoteEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/BaseTextNoteEvent.kt @@ -20,15 +20,15 @@ */ package com.vitorpamplona.quartz.nip10Notes -import android.util.Log import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip01Core.tags.addressables.taggedAddresses import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUsers +import com.vitorpamplona.quartz.nip10Notes.content.findIndexTagsWithEventsOrAddresses +import com.vitorpamplona.quartz.nip10Notes.content.findIndexTagsWithPeople import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser -import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser.nip19regex import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress import com.vitorpamplona.quartz.nip19Bech32.entities.NEmbed import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent @@ -39,10 +39,6 @@ import com.vitorpamplona.quartz.nip19Bech32.parse import com.vitorpamplona.quartz.nip19Bech32.parseAtag import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent import com.vitorpamplona.quartz.nip72ModCommunities.CommunityDefinitionEvent -import java.util.regex.Pattern - -val tagSearch = Pattern.compile("(?:\\s|\\A)\\#\\[([0-9]+)\\]") -val hashtagSearch = Pattern.compile("(?:\\s|\\A)#([^\\s!@#\$%^&*()=+./,\\[{\\]};:'\"?><]+)") @Immutable open class BaseTextNoteEvent( @@ -111,44 +107,19 @@ open class BaseTextNoteEvent( return it } - val matcher = tagSearch.matcher(content) - val returningList = mutableSetOf() - while (matcher.find()) { - try { - val tag = matcher.group(1)?.let { tags[it.toInt()] } - if (tag != null && tag.size > 1 && tag[0] == "p") { - returningList.add(tag[1]) - } - } catch (e: Exception) { + val citedUsers = mutableSetOf() + + findIndexTagsWithPeople(content, tags, citedUsers) + + Nip19Parser.parseAll(content).forEach { parsed -> + when (parsed) { + is NProfile -> citedUsers.add(parsed.hex) + is NPub -> citedUsers.add(parsed.hex) } } - val matcher2 = nip19regex.matcher(content) - while (matcher2.find()) { - val type = matcher2.group(2) // npub1 - val key = matcher2.group(3) // bech32 - val additionalChars = matcher2.group(4) // additional chars - - try { - if (type != null) { - val parsed = Nip19Parser.parseComponents(type, key, additionalChars)?.entity - - if (parsed != null) { - if (parsed is NProfile) { - returningList.add(parsed.hex) - } - if (parsed is NPub) { - returningList.add(parsed.hex) - } - } - } - } catch (e: Exception) { - Log.w("Unable to parse cited users that matched a NIP19 regex", e) - } - } - - citedUsersCache = returningList - return returningList + citedUsersCache = citedUsers + return citedUsers } fun findCitations(): Set { @@ -156,39 +127,16 @@ open class BaseTextNoteEvent( return it } - val citations = mutableSetOf() - // Removes citations from replies: - val matcher = tagSearch.matcher(content) - while (matcher.find()) { - try { - val tag = matcher.group(1)?.let { tags[it.toInt()] } - if (tag != null && tag.size > 1 && tag[0] == "e") { - citations.add(tag[1]) - } - if (tag != null && tag.size > 1 && tag[0] == "a") { - citations.add(tag[1]) - } - } catch (e: Exception) { - } - } + val citations = mutableSetOf() - val matcher2 = nip19regex.matcher(content) - while (matcher2.find()) { - val type = matcher2.group(2) // npub1 - val key = matcher2.group(3) // bech32 - val additionalChars = matcher2.group(4) // additional chars + findIndexTagsWithEventsOrAddresses(content, tags, citations).toMutableSet() - if (type != null) { - val parsed = Nip19Parser.parseComponents(type, key, additionalChars)?.entity - - if (parsed != null) { - when (parsed) { - is NEvent -> citations.add(parsed.hex) - is NAddress -> citations.add(parsed.aTag()) - is Note -> citations.add(parsed.hex) - is NEmbed -> citations.add(parsed.event.id) - } - } + Nip19Parser.parseAll(content).forEach { entity -> + when (entity) { + is NEvent -> citations.add(entity.hex) + is NAddress -> citations.add(entity.aTag()) + is Note -> citations.add(entity.hex) + is NEmbed -> citations.add(entity.event.id) } } @@ -227,18 +175,3 @@ open class BaseTextNoteEvent( } } } - -fun findHashtags(content: String): List { - val matcher = hashtagSearch.matcher(content) - val returningList = mutableSetOf() - while (matcher.find()) { - try { - val tag = matcher.group(1) - if (tag != null && tag.isNotBlank()) { - returningList.add(tag) - } - } catch (e: Exception) { - } - } - return returningList.toList() -} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/ETag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/ETag.kt index bb67c2f30..6c47865f0 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/ETag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/ETag.kt @@ -20,12 +20,9 @@ */ package com.vitorpamplona.quartz.nip10Notes -import android.util.Log import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.HexKey -import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent -import com.vitorpamplona.quartz.nip19Bech32.entities.Note import com.vitorpamplona.quartz.utils.bytesUsedInMemory import com.vitorpamplona.quartz.utils.pointerSizeInBytes import com.vitorpamplona.quartz.utils.removeTrailingNullsAndEmptyOthers @@ -52,21 +49,4 @@ data class ETag( fun toETagArray() = removeTrailingNullsAndEmptyOthers("e", eventId, relay, authorPubKeyHex) fun toQTagArray() = removeTrailingNullsAndEmptyOthers("q", eventId, relay, authorPubKeyHex) - - companion object { - fun parseNIP19(nevent: String): ETag? { - try { - val parsed = Nip19Parser.uriToRoute(nevent)?.entity - - return when (parsed) { - is Note -> ETag(parsed.hex) - is NEvent -> ETag(parsed.hex, parsed.author, parsed.relay.firstOrNull()) - else -> null - } - } catch (e: Throwable) { - Log.w("PTag", "Issue trying to Decode NIP19 $this: ${e.message}") - return null - } - } - } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/TextNoteEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/TextNoteEvent.kt index 2de5dd164..5d302ef95 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/TextNoteEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/TextNoteEvent.kt @@ -21,14 +21,16 @@ package com.vitorpamplona.quartz.nip10Notes import androidx.compose.runtime.Immutable -import com.linkedin.urls.detection.UrlDetector -import com.linkedin.urls.detection.UrlDetectorOptions import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip01Core.tags.geohash.geohashMipMap +import com.vitorpamplona.quartz.nip01Core.tags.hashtags.buildHashtagTags +import com.vitorpamplona.quartz.nip10Notes.content.buildUrlRefs +import com.vitorpamplona.quartz.nip10Notes.content.findHashtags +import com.vitorpamplona.quartz.nip10Notes.content.findURLs import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrl import com.vitorpamplona.quartz.nip57Zaps.ZapSplitSetup import com.vitorpamplona.quartz.nip92IMeta.IMetaTag @@ -112,18 +114,11 @@ class TextNoteEvent( ), ) } - findHashtags(msg).forEach { - val lowercaseTag = it.lowercase() - tags.add(arrayOf("t", it)) - if (it != lowercaseTag) { - tags.add(arrayOf("t", it.lowercase())) - } - } - extraTags?.forEach { tags.add(arrayOf("t", it)) } + tags.addAll(buildHashtagTags(findHashtags(msg) + (extraTags ?: emptyList()))) + tags.addAll(buildUrlRefs(findURLs(msg))) zapReceiver?.forEach { tags.add(arrayOf("zap", it.lnAddressOrPubKeyHex, it.relay ?: "", it.weight.toString())) } - findURLs(msg).forEach { tags.add(arrayOf("r", it)) } if (markAsSensitive) { tags.add(arrayOf("content-warning", "")) } @@ -141,8 +136,6 @@ class TextNoteEvent( } } -fun findURLs(text: String): List = UrlDetector(text, UrlDetectorOptions.Default).detect().map { it.originalUrl } - /** * Returns a list of NIP-10 marked tags that are also ordered at best effort to support the * deprecated method of positional tags to maximize backwards compatibility with clients that diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/content/ContentHashTags.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/content/ContentHashTags.kt new file mode 100644 index 000000000..d44ca0a57 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/content/ContentHashTags.kt @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip10Notes.content + +import java.util.regex.Pattern + +val hashtagSearch = Pattern.compile("(?:\\s|\\A)#([^\\s!@#\$%^&*()=+./,\\[{\\]};:'\"?><]+)") + +fun findHashtags( + content: String, + output: MutableSet = mutableSetOf(), +): List { + val matcher = hashtagSearch.matcher(content) + while (matcher.find()) { + try { + val tag = matcher.group(1) + if (tag != null && tag.isNotBlank()) { + output.add(tag) + } + } catch (e: Exception) { + } + } + return output.toList() +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/content/IndexedTags.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/content/IndexedTags.kt new file mode 100644 index 000000000..8c3f1fbb7 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/content/IndexedTags.kt @@ -0,0 +1,72 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip10Notes.content + +import com.vitorpamplona.quartz.nip01Core.core.TagArray +import java.util.regex.Pattern + +val tagSearch = Pattern.compile("(?:\\s|\\A)\\#\\[([0-9]+)\\]") + +/** + * Returns the old-style [1] tag that pionts to an index in the tag array + */ +fun findIndexTagsWithPeople( + content: String, + tags: TagArray, + output: MutableSet = mutableSetOf(), +): List { + val matcher = tagSearch.matcher(content) + while (matcher.find()) { + try { + val tag = matcher.group(1)?.let { tags[it.toInt()] } + if (tag != null && tag.size > 1 && tag[0] == "p") { + output.add(tag[1]) + } + } catch (e: Exception) { + } + } + + return output.toList() +} + +/** + * Returns the old-style [1] tag that pionts to an index in the tag array + */ +fun findIndexTagsWithEventsOrAddresses( + content: String, + tags: TagArray, + output: MutableSet = mutableSetOf(), +): Set { + val matcher = tagSearch.matcher(content) + while (matcher.find()) { + try { + val tag = matcher.group(1)?.let { tags[it.toInt()] } + if (tag != null && tag.size > 1 && tag[0] == "e") { + output.add(tag[1]) + } + if (tag != null && tag.size > 1 && tag[0] == "a") { + output.add(tag[1]) + } + } catch (e: Exception) { + } + } + return output +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/content/Urls.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/content/Urls.kt new file mode 100644 index 000000000..05b38990f --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/content/Urls.kt @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip10Notes.content + +import com.linkedin.urls.detection.UrlDetector +import com.linkedin.urls.detection.UrlDetectorOptions +import com.vitorpamplona.quartz.nip96FileStorage.HttpUrlFormatter + +fun findURLs(text: String): List = UrlDetector(text, UrlDetectorOptions.Default).detect().map { it.originalUrl } + +fun buildUrlRefs(urls: List): List> = + urls + .mapTo(HashSet()) { url -> + HttpUrlFormatter.normalize(url) + }.map { + arrayOf("r", it) + } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/Nip19Parser.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/Nip19Parser.kt index 9b54bb838..be66f40eb 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/Nip19Parser.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/Nip19Parser.kt @@ -129,6 +129,25 @@ object Nip19Parser { Log.w("NIP19 Parser", "Issue trying to Decode NIP19 $key: ${e.message}", e) null } + + fun parseAll(content: String): List { + val matcher2 = nip19regex.matcher(content) + val returningList = mutableListOf() + while (matcher2.find()) { + val type = matcher2.group(2) // npub1 + val key = matcher2.group(3) // bech32 + val additionalChars = matcher2.group(4) // additional chars + + if (type != null) { + val parsed = Nip19Parser.parseComponents(type, key, additionalChars)?.entity + + if (parsed != null) { + returningList.add(parsed) + } + } + } + return returningList + } } fun decodePublicKey(key: String): ByteArray = diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/CommentEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/CommentEvent.kt index 37fe5f4da..c77821328 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/CommentEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/CommentEvent.kt @@ -28,11 +28,13 @@ import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip01Core.tags.geohash.geohashMipMap +import com.vitorpamplona.quartz.nip01Core.tags.hashtags.buildHashtagTags import com.vitorpamplona.quartz.nip10Notes.BaseTextNoteEvent import com.vitorpamplona.quartz.nip10Notes.ETag import com.vitorpamplona.quartz.nip10Notes.PTag -import com.vitorpamplona.quartz.nip10Notes.findHashtags -import com.vitorpamplona.quartz.nip10Notes.findURLs +import com.vitorpamplona.quartz.nip10Notes.content.buildUrlRefs +import com.vitorpamplona.quartz.nip10Notes.content.findHashtags +import com.vitorpamplona.quartz.nip10Notes.content.findURLs import com.vitorpamplona.quartz.nip19Bech32.parseAtag import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrl import com.vitorpamplona.quartz.nip57Zaps.ZapSplitSetup @@ -205,15 +207,8 @@ class CommentEvent( addressesMentioned.forEach { tags.add(it.toQTagArray()) } eventsMentioned.forEach { tags.add(it.toQTagArray()) } - findHashtags(msg).forEach { - val lowercaseTag = it.lowercase() - tags.add(arrayOf("t", it)) - if (it != lowercaseTag) { - tags.add(arrayOf("t", it.lowercase())) - } - } - - findURLs(msg).forEach { tags.add(arrayOf("r", it)) } + tags.addAll(buildHashtagTags(findHashtags(msg))) + tags.addAll(buildUrlRefs(findURLs(msg))) emojis?.forEach { tags.add(it.toTagArray()) } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/GitReplyEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/GitReplyEvent.kt index 587c0b05b..e34c617dc 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/GitReplyEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/GitReplyEvent.kt @@ -27,9 +27,11 @@ import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip01Core.tags.geohash.geohashMipMap +import com.vitorpamplona.quartz.nip01Core.tags.hashtags.buildHashtagTags import com.vitorpamplona.quartz.nip10Notes.BaseTextNoteEvent -import com.vitorpamplona.quartz.nip10Notes.findHashtags -import com.vitorpamplona.quartz.nip10Notes.findURLs +import com.vitorpamplona.quartz.nip10Notes.content.buildUrlRefs +import com.vitorpamplona.quartz.nip10Notes.content.findHashtags +import com.vitorpamplona.quartz.nip10Notes.content.findURLs import com.vitorpamplona.quartz.nip10Notes.positionalMarkedTags import com.vitorpamplona.quartz.nip19Bech32.parse import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrl @@ -93,7 +95,6 @@ class GitReplyEvent( replyTos: List? = null, mentions: List? = null, addresses: List? = null, - extraTags: List? = null, zapReceiver: List? = null, markAsSensitive: Boolean = false, zapRaiserAmount: Long? = null, @@ -146,15 +147,11 @@ class GitReplyEvent( ), ) } - findHashtags(msg).forEach { - tags.add(arrayOf("t", it)) - tags.add(arrayOf("t", it.lowercase())) - } - extraTags?.forEach { tags.add(arrayOf("t", it)) } + tags.addAll(buildHashtagTags(findHashtags(msg))) + tags.addAll(buildUrlRefs(findURLs(msg))) zapReceiver?.forEach { tags.add(arrayOf("zap", it.lnAddressOrPubKeyHex, it.relay ?: "", it.weight.toString())) } - findURLs(msg).forEach { tags.add(arrayOf("r", it)) } if (markAsSensitive) { tags.add(arrayOf("content-warning", "")) } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip35Torrents/TorrentCommentEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip35Torrents/TorrentCommentEvent.kt index 3ec3a7be7..c08938fe2 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip35Torrents/TorrentCommentEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip35Torrents/TorrentCommentEvent.kt @@ -27,9 +27,11 @@ import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip01Core.tags.geohash.geohashMipMap +import com.vitorpamplona.quartz.nip01Core.tags.hashtags.buildHashtagTags import com.vitorpamplona.quartz.nip10Notes.BaseTextNoteEvent -import com.vitorpamplona.quartz.nip10Notes.findHashtags -import com.vitorpamplona.quartz.nip10Notes.findURLs +import com.vitorpamplona.quartz.nip10Notes.content.buildUrlRefs +import com.vitorpamplona.quartz.nip10Notes.content.findHashtags +import com.vitorpamplona.quartz.nip10Notes.content.findURLs import com.vitorpamplona.quartz.nip10Notes.positionalMarkedTags import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrl import com.vitorpamplona.quartz.nip57Zaps.ZapSplitSetup @@ -62,7 +64,6 @@ class TorrentCommentEvent( replyTos: List? = null, mentions: List? = null, addresses: List? = null, - extraTags: List? = null, zapReceiver: List? = null, signer: NostrSigner, createdAt: Long = TimeUtils.now(), @@ -116,18 +117,13 @@ class TorrentCommentEvent( ), ) } - findHashtags(message).forEach { - val lowercaseTag = it.lowercase() - tags.add(arrayOf("t", it)) - if (it != lowercaseTag) { - tags.add(arrayOf("t", it.lowercase())) - } - } - extraTags?.forEach { tags.add(arrayOf("t", it)) } + tags.addAll(buildHashtagTags(findHashtags(message))) + tags.addAll(buildUrlRefs(findURLs(message))) + zapReceiver?.forEach { tags.add(arrayOf("zap", it.lnAddressOrPubKeyHex, it.relay ?: "", it.weight.toString())) } - findURLs(message).forEach { tags.add(arrayOf("r", it)) } + if (markAsSensitive) { tags.add(arrayOf("content-warning", "")) } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip68Picture/PictureEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip68Picture/PictureEvent.kt index fe8e82fba..707141b7c 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip68Picture/PictureEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip68Picture/PictureEvent.kt @@ -26,10 +26,12 @@ import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip01Core.tags.geohash.geohashMipMap +import com.vitorpamplona.quartz.nip01Core.tags.hashtags.buildHashtagTags import com.vitorpamplona.quartz.nip10Notes.ETag import com.vitorpamplona.quartz.nip10Notes.PTag -import com.vitorpamplona.quartz.nip10Notes.findHashtags -import com.vitorpamplona.quartz.nip10Notes.findURLs +import com.vitorpamplona.quartz.nip10Notes.content.buildUrlRefs +import com.vitorpamplona.quartz.nip10Notes.content.findHashtags +import com.vitorpamplona.quartz.nip10Notes.content.findURLs import com.vitorpamplona.quartz.nip22Comments.RootScope import com.vitorpamplona.quartz.nip57Zaps.ZapSplitSetup import com.vitorpamplona.quartz.nip92IMeta.Nip92MediaAttachments.Companion.IMETA @@ -182,15 +184,8 @@ class PictureEvent( eventsMentioned.forEach { tags.add(it.toQTagArray()) } if (msg != null) { - findHashtags(msg).forEach { - val lowercaseTag = it.lowercase() - tags.add(arrayOf("t", it)) - if (it != lowercaseTag) { - tags.add(arrayOf("t", it.lowercase())) - } - } - - findURLs(msg).forEach { tags.add(arrayOf("r", it)) } + tags.addAll(buildHashtagTags(findHashtags(msg))) + tags.addAll(buildUrlRefs(findURLs(msg))) } zapReceiver?.forEach { diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip99Classifieds/ClassifiedsEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip99Classifieds/ClassifiedsEvent.kt index 5e9641ae3..62bcc4ce4 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip99Classifieds/ClassifiedsEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip99Classifieds/ClassifiedsEvent.kt @@ -26,8 +26,9 @@ import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip01Core.tags.geohash.geohashMipMap -import com.vitorpamplona.quartz.nip10Notes.findHashtags -import com.vitorpamplona.quartz.nip10Notes.findURLs +import com.vitorpamplona.quartz.nip01Core.tags.hashtags.buildHashtagTags +import com.vitorpamplona.quartz.nip10Notes.content.findHashtags +import com.vitorpamplona.quartz.nip10Notes.content.findURLs import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrl import com.vitorpamplona.quartz.nip57Zaps.ZapSplitSetup import com.vitorpamplona.quartz.nip92IMeta.IMetaTag @@ -169,11 +170,7 @@ class ClassifiedsEvent( location?.let { tags.add(arrayOf("location", it)) } publishedAt?.let { tags.add(arrayOf("publishedAt", it.toString())) } condition?.let { tags.add(arrayOf("condition", it.value)) } - - findHashtags(message).forEach { - tags.add(arrayOf("t", it)) - tags.add(arrayOf("t", it.lowercase())) - } + tags.addAll(buildHashtagTags(findHashtags(message))) zapReceiver?.forEach { tags.add(arrayOf("zap", it.lnAddressOrPubKeyHex, it.relay ?: "", it.weight.toString())) }