NIP-30: add optional emoji-set address and shortcode validation
Per NIP-30 the emoji tag is ["emoji", <shortcode>, <url>, <emoji-set-address>] with the fourth field optional. EmojiUrlTag only exposed the first three. Adds emojiSet as an optional field, preserves existing positional callers via default null, and introduces isValidShortcode() so callers can validate user input against the spec (alphanumeric, hyphens, underscores).
This commit is contained in:
+13
-2
@@ -26,16 +26,27 @@ import androidx.compose.runtime.Immutable
|
||||
data class EmojiUrlTag(
|
||||
val code: String,
|
||||
val url: String,
|
||||
val emojiSet: String? = null,
|
||||
) {
|
||||
fun encode(): String = ":$code:$url"
|
||||
|
||||
fun toContentEncode(): String = contentEncode(code)
|
||||
|
||||
fun toTagArray() = arrayOf("emoji", code, url)
|
||||
fun toTagArray() =
|
||||
if (emojiSet != null) {
|
||||
arrayOf(TAG_NAME, code, url, emojiSet)
|
||||
} else {
|
||||
arrayOf(TAG_NAME, code, url)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TAG_NAME = "emoji"
|
||||
|
||||
// NIP-30: shortcode MUST be comprised of only alphanumeric characters, hyphens, and underscores.
|
||||
private val SHORTCODE_PATTERN = Regex("^[A-Za-z0-9_\\-]+$")
|
||||
|
||||
fun isValidShortcode(shortcode: String): Boolean = SHORTCODE_PATTERN.matches(shortcode)
|
||||
|
||||
fun contentEncode(code: String): String = ":$code:"
|
||||
|
||||
fun decode(encodedEmojiSetup: String): EmojiUrlTag? {
|
||||
@@ -49,7 +60,7 @@ data class EmojiUrlTag(
|
||||
|
||||
fun parse(tag: Array<String>): EmojiUrlTag? =
|
||||
if (tag.size > 2 && tag[0] == TAG_NAME) {
|
||||
EmojiUrlTag(tag[1], tag[2])
|
||||
EmojiUrlTag(tag[1], tag[2], tag.getOrNull(3))
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
+6
@@ -25,4 +25,10 @@ import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
||||
|
||||
fun <T : Event> TagArrayBuilder<T>.emoji(tag: EmojiUrlTag) = add(tag.toTagArray())
|
||||
|
||||
fun <T : Event> TagArrayBuilder<T>.emoji(
|
||||
code: String,
|
||||
url: String,
|
||||
emojiSet: String? = null,
|
||||
) = add(EmojiUrlTag(code, url, emojiSet).toTagArray())
|
||||
|
||||
fun <T : Event> TagArrayBuilder<T>.emojis(tags: List<EmojiUrlTag>) = addAll(tags.map { it.toTagArray() })
|
||||
|
||||
@@ -21,8 +21,11 @@
|
||||
package com.vitorpamplona.quartz.nip30CustomEmoji
|
||||
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertContentEquals
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class Nip30Test {
|
||||
@Test()
|
||||
@@ -164,4 +167,55 @@ class Nip30Test {
|
||||
assertEquals("https://media.misskeyusercontent.com/misskey/f6294900-f678-43cc-bc36-3ee5deeca4c2.gif", (result[i++] as CustomEmoji.ImageUrlType).url)
|
||||
assertEquals("\u200B\n#ioメシヨソイゲーム\nhttps://misskey.io/play/9g3qza4jow", (result[i] as CustomEmoji.TextType).text)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun emojiTagWithoutEmojiSetRoundtrip() {
|
||||
val tag = EmojiUrlTag("soapbox", "http://soapbox")
|
||||
assertContentEquals(arrayOf("emoji", "soapbox", "http://soapbox"), tag.toTagArray())
|
||||
assertEquals(tag, EmojiUrlTag.parse(tag.toTagArray()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun emojiTagWithEmojiSetRoundtrip() {
|
||||
val setAddress = "30030:abc123:emojis-fall"
|
||||
val tag = EmojiUrlTag("soapbox", "http://soapbox", setAddress)
|
||||
assertContentEquals(arrayOf("emoji", "soapbox", "http://soapbox", setAddress), tag.toTagArray())
|
||||
assertEquals(tag, EmojiUrlTag.parse(tag.toTagArray()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parseIgnoresTrailingFields() {
|
||||
val tag = arrayOf("emoji", "soapbox", "http://soapbox", "30030:abc123:pack", "extra")
|
||||
val parsed = EmojiUrlTag.parse(tag)
|
||||
assertEquals(EmojiUrlTag("soapbox", "http://soapbox", "30030:abc123:pack"), parsed)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parseRejectsWrongTagName() {
|
||||
assertNull(EmojiUrlTag.parse(arrayOf("t", "soapbox", "http://soapbox")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parseRejectsTooFewFields() {
|
||||
assertNull(EmojiUrlTag.parse(arrayOf("emoji", "soapbox")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun validShortcodesAcceptAlphanumericUnderscoreHyphen() {
|
||||
assertTrue(EmojiUrlTag.isValidShortcode("soapbox"))
|
||||
assertTrue(EmojiUrlTag.isValidShortcode("Soap_Box-123"))
|
||||
assertTrue(EmojiUrlTag.isValidShortcode("ABC"))
|
||||
assertTrue(EmojiUrlTag.isValidShortcode("007"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun invalidShortcodesRejected() {
|
||||
assertFalse(EmojiUrlTag.isValidShortcode(""))
|
||||
assertFalse(EmojiUrlTag.isValidShortcode("soap box"))
|
||||
assertFalse(EmojiUrlTag.isValidShortcode("soap:box"))
|
||||
assertFalse(EmojiUrlTag.isValidShortcode("soap.box"))
|
||||
assertFalse(EmojiUrlTag.isValidShortcode("soap/box"))
|
||||
assertFalse(EmojiUrlTag.isValidShortcode("soap!"))
|
||||
assertFalse(EmojiUrlTag.isValidShortcode("絵文字"))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user