Merge remote-tracking branch 'origin/main' into claude/nip88-polls-quartz-p5fBa
This commit is contained in:
@@ -370,7 +370,7 @@ mavenPublishing {
|
||||
coordinates(
|
||||
groupId = "com.vitorpamplona.quartz",
|
||||
artifactId = "quartz",
|
||||
version = "1.08.0",
|
||||
version = "1.09.2",
|
||||
)
|
||||
|
||||
// Configure publishing to Maven Central
|
||||
|
||||
+2
@@ -26,6 +26,8 @@ fun Event.anyHashTag(onEach: (str: String) -> Boolean) = tags.anyHashTag(onEach)
|
||||
|
||||
fun Event.countHashtags() = tags.countHashtags()
|
||||
|
||||
fun Event.hasMoreHashtagsThan(limit: Int) = tags.hasMoreHashtagsThan(limit)
|
||||
|
||||
fun Event.hasHashtags() = tags.hasHashtags()
|
||||
|
||||
fun Event.hashtags() = tags.hashtags()
|
||||
|
||||
+7
@@ -46,6 +46,13 @@ class HashtagTag {
|
||||
return tag[1]
|
||||
}
|
||||
|
||||
fun parseLowercase(tag: Array<String>): String? {
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].isNotEmpty()) { return null }
|
||||
return tag[1].lowercase()
|
||||
}
|
||||
|
||||
fun assemble(name: String) = arrayOf(TAG_NAME, name)
|
||||
|
||||
fun assembleDualCase(name: String): List<Array<String>> {
|
||||
|
||||
+21
@@ -38,8 +38,29 @@ fun TagArray.hashtags() = this.mapNotNull(HashtagTag::parse)
|
||||
|
||||
fun TagArray.countHashtags() = this.count(HashtagTag::isTagged)
|
||||
|
||||
fun TagArray.hasMoreHashtagsThan(limit: Int): Boolean {
|
||||
val count = this.count(HashtagTag::isTagged)
|
||||
return count > limit && this.countUnique(count, HashtagTag::parseLowercase) > limit
|
||||
}
|
||||
|
||||
fun TagArray.isTaggedHash(hashtag: String) = this.isTagged(HashtagTag.TAG_NAME, hashtag, true)
|
||||
|
||||
fun TagArray.isTaggedHashes(hashtags: Set<String>) = this.isAnyLowercaseTagged(HashtagTag.TAG_NAME, hashtags)
|
||||
|
||||
fun TagArray.firstIsTaggedHashes(hashtags: Set<String>) = this.firstAnyLowercaseTaggedValue(HashtagTag.TAG_NAME, hashtags)
|
||||
|
||||
public inline fun <T, U> Array<out T>.countUnique(
|
||||
size: Int,
|
||||
transform: (T) -> U?,
|
||||
): Int {
|
||||
// Pre-allocate hash set to avoid frequent resizing for larger arrays
|
||||
val seen = HashSet<U>(size)
|
||||
var count = 0
|
||||
for (element in this) {
|
||||
val value = transform(element)
|
||||
if (value != null && seen.add(value)) {
|
||||
++count
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
+18
-6
@@ -47,14 +47,26 @@ data class Nip05Id(
|
||||
fun toDomainUrl(): String = domainUrl(domain)
|
||||
|
||||
companion object {
|
||||
// NIP-05 localpart: dot-separated atoms of [a-z0-9_-]. Forbids leading,
|
||||
// trailing, and consecutive dots (NIP-05 + RFC 5321 local-part rules).
|
||||
private val LOCAL_PART_REGEX = Regex("^[a-z0-9_-]+(\\.[a-z0-9_-]+)*$")
|
||||
|
||||
// Hostname: dot-separated labels of [a-z0-9-], no leading/trailing hyphen,
|
||||
// require at least one dot so single-label garbage (e.g. "s!ayer") is rejected.
|
||||
private val DOMAIN_REGEX =
|
||||
Regex("^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)(\\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)+$")
|
||||
|
||||
fun parse(nip05address: String): Nip05Id? {
|
||||
val parts = nip05address.trim().lowercase().split("@")
|
||||
|
||||
return when (parts.size) {
|
||||
2 -> Nip05Id(parts[0], parts[1])
|
||||
1 -> Nip05Id(parts[0], "_")
|
||||
else -> null
|
||||
}
|
||||
if (parts.size != 2) return null
|
||||
val name = parts[0]
|
||||
val domain = parts[1]
|
||||
if (!LOCAL_PART_REGEX.matches(name)) return null
|
||||
if (!DOMAIN_REGEX.matches(domain)) return null
|
||||
// Reject IP literals — NIP-05 expects a hostname, and a digits-only
|
||||
// TLD is the cheapest way to tell them apart from real domains.
|
||||
if (domain.substringAfterLast('.').all { it.isDigit() }) return null
|
||||
return Nip05Id(name, domain)
|
||||
}
|
||||
|
||||
fun assemble(
|
||||
|
||||
@@ -181,6 +181,9 @@ class CommentEvent(
|
||||
|
||||
fun isScoped(scopeTest: (String) -> Boolean) = tags.any { RootIdentifierTag.isTagged(it, scopeTest) || ReplyIdentifierTag.isTagged(it, scopeTest) }
|
||||
|
||||
/** True when the comment points at an external identifier (`I` tag), e.g. a hashtag, geohash or url. */
|
||||
fun hasRootScopeIdentifier() = tags.any { RootIdentifierTag.match(it) }
|
||||
|
||||
fun hasRootScopeKind(kind: String) = tags.any(RootKindTag::isKind, kind)
|
||||
|
||||
fun hasReplyScopeKind(kind: String) = tags.any(ReplyKindTag::isKind, kind)
|
||||
|
||||
+60
@@ -94,6 +94,66 @@ class Nip05Test {
|
||||
assertNull(parsedNip05)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `parse rejects empty localpart`() {
|
||||
assertNull(Nip05Id.parse("@example.com"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `parse rejects empty domain`() {
|
||||
assertNull(Nip05Id.parse("alice@"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `parse rejects domain without a dot`() {
|
||||
assertNull(Nip05Id.parse("alice@localhost"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `parse rejects domain with illegal character`() {
|
||||
assertNull(Nip05Id.parse("_@s!ayer"))
|
||||
assertNull(Nip05Id.parse("@s!ayer"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `parse rejects localpart with illegal character`() {
|
||||
assertNull(Nip05Id.parse("al!ce@example.com"))
|
||||
assertNull(Nip05Id.parse("alice space@example.com"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `parse rejects bare string without at-sign`() {
|
||||
assertNull(Nip05Id.parse("alice"))
|
||||
assertNull(Nip05Id.parse("example.com"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `parse rejects domain label with leading or trailing hyphen`() {
|
||||
assertNull(Nip05Id.parse("alice@-example.com"))
|
||||
assertNull(Nip05Id.parse("alice@example-.com"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `parse rejects localpart with leading trailing or consecutive dots`() {
|
||||
assertNull(Nip05Id.parse(".alice@example.com"))
|
||||
assertNull(Nip05Id.parse("alice.@example.com"))
|
||||
assertNull(Nip05Id.parse("alice..bob@example.com"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `parse rejects IPv4 literal as domain`() {
|
||||
assertNull(Nip05Id.parse("alice@192.168.1.1"))
|
||||
assertNull(Nip05Id.parse("alice@8.8.8.8"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `parse accepts wildcard underscore localpart`() {
|
||||
val nip05 = Nip05Id.parse("_@example.com")
|
||||
assertNotNull(nip05)
|
||||
assertEquals("_", nip05.name)
|
||||
assertEquals("example.com", nip05.domain)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `execute assemble url with valid value returns nip05 url`() {
|
||||
// given
|
||||
|
||||
-8
@@ -21,7 +21,6 @@
|
||||
package com.vitorpamplona.quartz.nip06KeyDerivation
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
||||
import kotlin.test.Ignore
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
@@ -57,11 +56,4 @@ class Nip06CommonTest {
|
||||
val privateKeyHex21 = nip06.privateKeyFromMnemonic(menemonic1, 42).toHexKey()
|
||||
assertEquals("ad993054383da74e955f8b86346365b5ffd6575992e1de3738dda9f94407052b", privateKeyHex21)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore()
|
||||
fun fromSeedNip06FromSnort() {
|
||||
val privateKeyNsec = nip06.privateKeyFromMnemonic(snortTest).toHexKey()
|
||||
assertEquals("nsec1ppw9ltr2x9qwg9a2qnmgv98tfruy2ejnja7me76mwmsreu3s8u2sscj5nt", privateKeyNsec)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user