mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-05 19:28:25 +00:00
Merge pull request #3166 from davotoula/fix/nip71-legacy-video-addressable-dtag
Compute legacy NIP-71 video addresses with their d tag
This commit is contained in:
+2
-2
@@ -32,8 +32,8 @@ import com.vitorpamplona.amethyst.ui.dal.FilterByListParams
|
||||
import com.vitorpamplona.quartz.experimental.profileGallery.ProfileGalleryEntryEvent
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.clip.LiveActivitiesClipEvent
|
||||
import com.vitorpamplona.quartz.nip68Picture.PictureEvent
|
||||
import com.vitorpamplona.quartz.nip71Video.AddressableVideoEvent
|
||||
import com.vitorpamplona.quartz.nip71Video.RegularVideoEvent
|
||||
import com.vitorpamplona.quartz.nip71Video.ReplaceableVideoEvent
|
||||
import com.vitorpamplona.quartz.nip71Video.VideoVerticalEvent
|
||||
|
||||
class UserProfileGalleryFeedFilter(
|
||||
@@ -81,7 +81,7 @@ class UserProfileGalleryFeedFilter(
|
||||
(
|
||||
noteEvent is PictureEvent ||
|
||||
noteEvent is RegularVideoEvent ||
|
||||
(noteEvent is ReplaceableVideoEvent && it is AddressableNote) ||
|
||||
(noteEvent is AddressableVideoEvent && it is AddressableNote) ||
|
||||
(noteEvent is ProfileGalleryEntryEvent && noteEvent.hasUrl() && noteEvent.hasFromEvent())
|
||||
)
|
||||
|
||||
|
||||
+3
-3
@@ -21,7 +21,7 @@
|
||||
package com.vitorpamplona.quartz.nip71Video
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.nip01Core.core.BaseReplaceableEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.events.ETag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags
|
||||
@@ -37,7 +37,7 @@ import com.vitorpamplona.quartz.nip94FileMetadata.tags.HashSha256Tag
|
||||
import com.vitorpamplona.quartz.nip94FileMetadata.tags.MimeTypeTag
|
||||
|
||||
@Immutable
|
||||
abstract class ReplaceableVideoEvent(
|
||||
abstract class AddressableVideoEvent(
|
||||
id: HexKey,
|
||||
pubKey: HexKey,
|
||||
createdAt: Long,
|
||||
@@ -45,7 +45,7 @@ abstract class ReplaceableVideoEvent(
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : BaseReplaceableEvent(id, pubKey, createdAt, kind, tags, content, sig),
|
||||
) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig),
|
||||
PublishedAtProvider,
|
||||
VideoEvent,
|
||||
RootScope {
|
||||
+1
-1
@@ -40,7 +40,7 @@ class VideoHorizontalEvent(
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : ReplaceableVideoEvent(id, pubKey, createdAt, KIND, tags, content, sig),
|
||||
) : AddressableVideoEvent(id, pubKey, createdAt, KIND, tags, content, sig),
|
||||
RootScope {
|
||||
companion object {
|
||||
const val KIND = 34235
|
||||
|
||||
+1
-1
@@ -40,7 +40,7 @@ class VideoVerticalEvent(
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : ReplaceableVideoEvent(id, pubKey, createdAt, KIND, tags, content, sig),
|
||||
) : AddressableVideoEvent(id, pubKey, createdAt, KIND, tags, content, sig),
|
||||
RootScope {
|
||||
companion object {
|
||||
const val KIND = 34236
|
||||
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* Copyright (c) 2025 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.nip71Video
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Address
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
/**
|
||||
* Kinds 34235/34236 (legacy NIP-71 videos) are parameterized replaceable
|
||||
* events: their address MUST include the `d` tag. A wrong (empty-dTag)
|
||||
* address makes LocalCache consume the event into a different
|
||||
* AddressableNote than the one `a` tags point to, so quotes/reposts of
|
||||
* these videos never resolve on screen.
|
||||
*/
|
||||
class AddressableVideoEventAddressTest {
|
||||
// Fixture from a real kind-34236 event published by the Divine client
|
||||
// (bfe2f224…, "Lunchtime for our Koi"); the horizontal test reuses the
|
||||
// same data synthetically.
|
||||
private val pubkey = "3b6187c08b9dd5617150ea047e788a0fdd44b4394cb5566cba76f683ddc027d2"
|
||||
private val dTag = "7af7cae314483a84dcc204824cef10aace246a69c819734412330e2a25f459a1"
|
||||
|
||||
private fun assertAddressUsesDTag(
|
||||
kind: Int,
|
||||
event: AddressableVideoEvent,
|
||||
) {
|
||||
assertEquals(dTag, event.dTag())
|
||||
assertEquals(Address(kind, pubkey, dTag), event.address())
|
||||
// The kind:pubkey:dTag wire format is fixed by NIP-01, so it is
|
||||
// asserted literally instead of via Address.assemble (which is
|
||||
// what addressTag() calls internally).
|
||||
assertEquals("$kind:$pubkey:$dTag", event.addressTag())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun verticalVideoAddressUsesDTag() {
|
||||
val event =
|
||||
VideoVerticalEvent(
|
||||
id = "bfe2f2244fefc7cebc7b2eae825495f99dabb4649ee3f90ab1fa33bcd1e9bb9f",
|
||||
pubKey = pubkey,
|
||||
createdAt = 1780894816,
|
||||
tags = arrayOf(arrayOf("d", dTag), arrayOf("title", "Lunchtime for our Koi")),
|
||||
content = "Lunchtime for our Koi",
|
||||
sig = "",
|
||||
)
|
||||
|
||||
assertAddressUsesDTag(VideoVerticalEvent.KIND, event)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun horizontalVideoAddressUsesDTag() {
|
||||
val event =
|
||||
VideoHorizontalEvent(
|
||||
id = "bfe2f2244fefc7cebc7b2eae825495f99dabb4649ee3f90ab1fa33bcd1e9bb9f",
|
||||
pubKey = pubkey,
|
||||
createdAt = 1780894816,
|
||||
tags = arrayOf(arrayOf("d", dTag)),
|
||||
content = "",
|
||||
sig = "",
|
||||
)
|
||||
|
||||
assertAddressUsesDTag(VideoHorizontalEvent.KIND, event)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun videoWithoutDTagFallsBackToEmptyAddress() {
|
||||
val event =
|
||||
VideoVerticalEvent(
|
||||
id = "bfe2f2244fefc7cebc7b2eae825495f99dabb4649ee3f90ab1fa33bcd1e9bb9f",
|
||||
pubKey = pubkey,
|
||||
createdAt = 1780894816,
|
||||
tags = arrayOf(arrayOf("title", "No d tag")),
|
||||
content = "",
|
||||
sig = "",
|
||||
)
|
||||
|
||||
assertEquals("", event.dTag())
|
||||
assertEquals(Address(VideoVerticalEvent.KIND, pubkey, ""), event.address())
|
||||
assertEquals("${VideoVerticalEvent.KIND}:$pubkey:", event.addressTag())
|
||||
}
|
||||
}
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* Copyright (c) 2025 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.utils
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
/**
|
||||
* Guards the kind-range vs class-hierarchy invariant across the whole
|
||||
* EventFactory: parameterized replaceables (30000..39999) must derive their
|
||||
* address from the `d` tag, while plain replaceables (10000..19999, plus
|
||||
* kinds 0 and 3) must ignore stray `d` tags. A typed class extending the
|
||||
* wrong base (the kind-34235/34236 bug, fixed in this branch) splits the
|
||||
* cache between the event's own address and the address `a` tags reference.
|
||||
*
|
||||
* Known offenders that predate this guard are allowlisted below so the test
|
||||
* catches NEW mismatches; shrink the lists as they get fixed.
|
||||
*/
|
||||
class EventFactoryKindRangeTest {
|
||||
/**
|
||||
* NIP-87 ecash kinds that are addressable per spec but currently extend
|
||||
* plain Event, so they have no address at all and never replace older
|
||||
* versions in the cache.
|
||||
*/
|
||||
private val knownNonAddressable = setOf(38000, 38172, 38173)
|
||||
|
||||
/**
|
||||
* NIP-51-style list kinds whose shared PrivateTagArrayEvent hierarchy
|
||||
* reads `d` tags even though plain replaceables must ignore them; a
|
||||
* stray `d` tag on a malformed event fragments their cache address.
|
||||
*/
|
||||
private val knownDTagReaders =
|
||||
setOf(
|
||||
10004,
|
||||
10005,
|
||||
10006,
|
||||
10007,
|
||||
10009,
|
||||
10012,
|
||||
10013,
|
||||
10015,
|
||||
10017,
|
||||
10018,
|
||||
10020,
|
||||
10023,
|
||||
10040,
|
||||
10054,
|
||||
10081,
|
||||
10086,
|
||||
10087,
|
||||
10088,
|
||||
10089,
|
||||
10090,
|
||||
10101,
|
||||
10102,
|
||||
)
|
||||
|
||||
private val probeDTag = "probe-d-tag"
|
||||
|
||||
private fun probe(kind: Int) = EventFactory.create<Event>("", "", 0L, kind, arrayOf(arrayOf("d", probeDTag)), "", "")
|
||||
|
||||
@Test
|
||||
fun addressableKindsReadTheirDTag() {
|
||||
val violations =
|
||||
(30000 until 40000).mapNotNull { kind ->
|
||||
val event = probe(kind)
|
||||
when {
|
||||
// Unknown kinds parse as a bare Event; only typed classes are checked.
|
||||
event::class == Event::class -> null
|
||||
kind in knownNonAddressable -> null
|
||||
event !is AddressableEvent -> "kind $kind (${event::class.simpleName}) does not implement AddressableEvent"
|
||||
event.dTag() != probeDTag -> "kind $kind (${event::class.simpleName}) ignores its d tag"
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
assertEquals(emptyList(), violations)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun plainReplaceableKindsIgnoreStrayDTags() {
|
||||
val violations =
|
||||
(listOf(0, 3) + (10000 until 20000)).mapNotNull { kind ->
|
||||
val event = probe(kind)
|
||||
if (kind !in knownDTagReaders && event::class != Event::class && event is AddressableEvent && event.dTag() != "") {
|
||||
"kind $kind (${event::class.simpleName}) addresses itself by a stray d tag"
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
assertEquals(emptyList(), violations)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user