mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-09 16:14:40 +00:00
Merge pull request #3473 from davotoula/feat/birdstar-detection-kind-2473
support Birdstar bird detection events (kind 2473)
This commit is contained in:
@@ -54,6 +54,7 @@ import com.vitorpamplona.quartz.experimental.attestations.recommendation.Attesto
|
||||
import com.vitorpamplona.quartz.experimental.attestations.request.AttestationRequestEvent
|
||||
import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent
|
||||
import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent
|
||||
import com.vitorpamplona.quartz.experimental.birdstar.BirdDetectionEvent
|
||||
import com.vitorpamplona.quartz.experimental.birdstar.BirdexEvent
|
||||
import com.vitorpamplona.quartz.experimental.edits.TextNoteModificationEvent
|
||||
import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent
|
||||
@@ -3489,6 +3490,10 @@ object LocalCache : ILocalCache, ICacheProvider {
|
||||
consumeBaseReplaceable(event, relay, wasVerified)
|
||||
}
|
||||
|
||||
is BirdDetectionEvent -> {
|
||||
consumeRegularEvent(event, relay, wasVerified)
|
||||
}
|
||||
|
||||
is CommentEvent -> {
|
||||
consumeRegularEvent(event, relay, wasVerified)
|
||||
}
|
||||
|
||||
@@ -118,6 +118,7 @@ import com.vitorpamplona.amethyst.ui.note.types.RenderAttestorRecommendation
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderAudioHeader
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderAudioTrack
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderBadgeAward
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderBirdDetection
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderBirdex
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderCalendarCollectionEvent
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderCalendarDateSlotEvent
|
||||
@@ -229,6 +230,7 @@ import com.vitorpamplona.quartz.experimental.attestations.recommendation.Attesto
|
||||
import com.vitorpamplona.quartz.experimental.attestations.request.AttestationRequestEvent
|
||||
import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent
|
||||
import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent
|
||||
import com.vitorpamplona.quartz.experimental.birdstar.BirdDetectionEvent
|
||||
import com.vitorpamplona.quartz.experimental.birdstar.BirdexEvent
|
||||
import com.vitorpamplona.quartz.experimental.bounties.bountyBaseReward
|
||||
import com.vitorpamplona.quartz.experimental.edits.TextNoteModificationEvent
|
||||
@@ -1381,6 +1383,10 @@ private fun RenderNoteRow(
|
||||
RenderBirdex(baseNote)
|
||||
}
|
||||
|
||||
is BirdDetectionEvent -> {
|
||||
RenderBirdDetection(baseNote)
|
||||
}
|
||||
|
||||
is RoadEventReportEvent -> {
|
||||
RenderRoadEventReport(baseNote)
|
||||
}
|
||||
|
||||
@@ -30,17 +30,24 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.ui.components.ClickableUrl
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.amethyst.ui.theme.replyModifier
|
||||
import com.vitorpamplona.quartz.experimental.birdstar.BirdDetectionEvent
|
||||
import com.vitorpamplona.quartz.experimental.birdstar.BirdexEvent
|
||||
|
||||
/** How many species names to list before collapsing into a "+N more" suffix. */
|
||||
private const val SPECIES_PREVIEW_LIMIT = 6
|
||||
|
||||
/** Bird emoji prefix shared by both Birdstar card titles. */
|
||||
private const val BIRD_PREFIX = "\uD83D\uDC26 "
|
||||
|
||||
/**
|
||||
* Minimal, fixed-size summary card for a Birdstar "Birdex" (kind 12473).
|
||||
*
|
||||
@@ -61,7 +68,7 @@ fun RenderBirdex(baseNote: Note) {
|
||||
|
||||
Column(MaterialTheme.colorScheme.replyModifier.padding(10.dp)) {
|
||||
Text(
|
||||
text = pluralStringResource(R.plurals.birdex_species_count, names.size, names.size),
|
||||
text = BIRD_PREFIX + pluralStringResource(R.plurals.birdex_species_count, names.size, names.size),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
)
|
||||
|
||||
@@ -82,3 +89,64 @@ fun RenderBirdex(baseNote: Note) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimal card for a single Birdstar bird detection (kind 2473).
|
||||
*
|
||||
* The event has no body and no images. The title is the common name parsed from
|
||||
* the publisher's `alt` tag (a generic label when absent), and the scientific
|
||||
* name renders as an italic link to the Wikidata species entry from the `i` tag
|
||||
* when one is present. The `g` geohash is surfaced by the generic note-location
|
||||
* UI, not here.
|
||||
*/
|
||||
@Composable
|
||||
fun RenderBirdDetection(baseNote: Note) {
|
||||
val noteEvent = baseNote.event as? BirdDetectionEvent ?: return
|
||||
|
||||
val detection =
|
||||
remember(noteEvent) {
|
||||
BirdDetectionInfo(
|
||||
commonName = noteEvent.commonName(),
|
||||
species = noteEvent.speciesName(),
|
||||
reference = noteEvent.speciesReference(),
|
||||
)
|
||||
}
|
||||
|
||||
Column(MaterialTheme.colorScheme.replyModifier.padding(10.dp)) {
|
||||
Text(
|
||||
text = BIRD_PREFIX + (detection.commonName ?: stringResource(R.string.bird_detection_title)),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
|
||||
if (detection.species != null) {
|
||||
Spacer(Modifier.height(6.dp))
|
||||
if (detection.reference != null) {
|
||||
val typography = MaterialTheme.typography
|
||||
val speciesStyle = remember(typography) { typography.bodyMedium.copy(fontStyle = FontStyle.Italic) }
|
||||
ClickableUrl(
|
||||
urlText = detection.species,
|
||||
url = detection.reference,
|
||||
style = speciesStyle,
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
text = detection.species,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontStyle = FontStyle.Italic,
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Tag values parsed once per event for the detection card. */
|
||||
private class BirdDetectionInfo(
|
||||
val commonName: String?,
|
||||
val species: String?,
|
||||
val reference: String?,
|
||||
)
|
||||
|
||||
+2
@@ -34,6 +34,7 @@ import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder
|
||||
import com.vitorpamplona.quartz.experimental.agora.FundraiserEvent
|
||||
import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent
|
||||
import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent
|
||||
import com.vitorpamplona.quartz.experimental.birdstar.BirdDetectionEvent
|
||||
import com.vitorpamplona.quartz.experimental.birdstar.BirdexEvent
|
||||
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent
|
||||
import com.vitorpamplona.quartz.experimental.music.playlist.MusicPlaylistEvent
|
||||
@@ -140,6 +141,7 @@ class FollowPackFeedNewThreadFeedFilter(
|
||||
noteEvent is ClassifiedsEvent ||
|
||||
noteEvent is FundraiserEvent ||
|
||||
noteEvent is BirdexEvent ||
|
||||
noteEvent is BirdDetectionEvent ||
|
||||
noteEvent.isRenderableRepost() ||
|
||||
(noteEvent is LongTextNoteEvent && noteEvent.content.isNotEmpty()) ||
|
||||
(noteEvent is WikiNoteEvent && noteEvent.content.isNotEmpty()) ||
|
||||
|
||||
+2
@@ -37,6 +37,7 @@ import com.vitorpamplona.quartz.experimental.attestations.recommendation.Attesto
|
||||
import com.vitorpamplona.quartz.experimental.attestations.request.AttestationRequestEvent
|
||||
import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent
|
||||
import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent
|
||||
import com.vitorpamplona.quartz.experimental.birdstar.BirdDetectionEvent
|
||||
import com.vitorpamplona.quartz.experimental.birdstar.BirdexEvent
|
||||
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent
|
||||
import com.vitorpamplona.quartz.experimental.music.playlist.MusicPlaylistEvent
|
||||
@@ -129,6 +130,7 @@ class HomeNewThreadFeedFilter(
|
||||
noteEvent is ClassifiedsEvent ||
|
||||
noteEvent is FundraiserEvent ||
|
||||
noteEvent is BirdexEvent ||
|
||||
noteEvent is BirdDetectionEvent ||
|
||||
noteEvent.isRenderableRepost() ||
|
||||
(noteEvent is LongTextNoteEvent && noteEvent.content.isNotEmpty()) ||
|
||||
(noteEvent is WikiNoteEvent && noteEvent.content.isNotEmpty()) ||
|
||||
|
||||
+4
@@ -24,6 +24,8 @@ import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsTopN
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsTopNavPerRelayFilterSet
|
||||
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
||||
import com.vitorpamplona.quartz.experimental.attestations.attestation.AttestationEvent
|
||||
import com.vitorpamplona.quartz.experimental.birdstar.BirdDetectionEvent
|
||||
import com.vitorpamplona.quartz.experimental.birdstar.BirdexEvent
|
||||
import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent
|
||||
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent
|
||||
import com.vitorpamplona.quartz.experimental.nipsOnNostr.NipTextEvent
|
||||
@@ -72,6 +74,8 @@ val HomePostsNewThreadKinds2 =
|
||||
ChessGameEvent.KIND,
|
||||
LiveChessGameChallengeEvent.KIND,
|
||||
LiveChessGameEndEvent.KIND,
|
||||
BirdDetectionEvent.KIND,
|
||||
BirdexEvent.KIND,
|
||||
)
|
||||
|
||||
val HomePostsConversationKinds =
|
||||
|
||||
+4
@@ -26,6 +26,8 @@ import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
||||
import com.vitorpamplona.quartz.experimental.attestations.attestation.AttestationEvent
|
||||
import com.vitorpamplona.quartz.experimental.birdstar.BirdDetectionEvent
|
||||
import com.vitorpamplona.quartz.experimental.birdstar.BirdexEvent
|
||||
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent
|
||||
import com.vitorpamplona.quartz.experimental.nipsOnNostr.NipTextEvent
|
||||
import com.vitorpamplona.quartz.experimental.zapPolls.ZapPollEvent
|
||||
@@ -74,6 +76,8 @@ val UserProfilePostKinds2 =
|
||||
ZapPollEvent.KIND,
|
||||
PinListEvent.KIND,
|
||||
AttestationEvent.KIND,
|
||||
BirdDetectionEvent.KIND,
|
||||
BirdexEvent.KIND,
|
||||
)
|
||||
|
||||
// NIP-5A nsites (15128/35128) and NIP-5D napplets (15129/35129) the user publishes, surfaced in
|
||||
|
||||
+2
@@ -31,6 +31,7 @@ import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder
|
||||
import com.vitorpamplona.quartz.experimental.agora.FundraiserEvent
|
||||
import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent
|
||||
import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent
|
||||
import com.vitorpamplona.quartz.experimental.birdstar.BirdDetectionEvent
|
||||
import com.vitorpamplona.quartz.experimental.birdstar.BirdexEvent
|
||||
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent
|
||||
import com.vitorpamplona.quartz.experimental.music.playlist.MusicPlaylistEvent
|
||||
@@ -80,6 +81,7 @@ class UserProfileMutualFeedFilter(
|
||||
it.event is ClassifiedsEvent ||
|
||||
it.event is FundraiserEvent ||
|
||||
it.event is BirdexEvent ||
|
||||
it.event is BirdDetectionEvent ||
|
||||
it.event.isRenderableRepost() ||
|
||||
it.event is LongTextNoteEvent ||
|
||||
it.event is WikiNoteEvent ||
|
||||
|
||||
+2
@@ -35,6 +35,7 @@ import com.vitorpamplona.quartz.experimental.attestations.recommendation.Attesto
|
||||
import com.vitorpamplona.quartz.experimental.attestations.request.AttestationRequestEvent
|
||||
import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent
|
||||
import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent
|
||||
import com.vitorpamplona.quartz.experimental.birdstar.BirdDetectionEvent
|
||||
import com.vitorpamplona.quartz.experimental.birdstar.BirdexEvent
|
||||
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent
|
||||
import com.vitorpamplona.quartz.experimental.music.playlist.MusicPlaylistEvent
|
||||
@@ -86,6 +87,7 @@ class UserProfileNewThreadFeedFilter(
|
||||
it.event is ClassifiedsEvent ||
|
||||
it.event is FundraiserEvent ||
|
||||
it.event is BirdexEvent ||
|
||||
it.event is BirdDetectionEvent ||
|
||||
it.event.isRenderableRepost() ||
|
||||
it.event is LongTextNoteEvent ||
|
||||
it.event is WikiNoteEvent ||
|
||||
|
||||
+4
@@ -155,6 +155,7 @@ import com.vitorpamplona.amethyst.ui.note.types.RenderAttestation
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderAttestationRequest
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderAttestorProficiency
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderAttestorRecommendation
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderBirdDetection
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderBirdex
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderCalendarDateSlotEvent
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderCalendarTimeSlotEvent
|
||||
@@ -249,6 +250,7 @@ import com.vitorpamplona.quartz.experimental.attestations.recommendation.Attesto
|
||||
import com.vitorpamplona.quartz.experimental.attestations.request.AttestationRequestEvent
|
||||
import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent
|
||||
import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent
|
||||
import com.vitorpamplona.quartz.experimental.birdstar.BirdDetectionEvent
|
||||
import com.vitorpamplona.quartz.experimental.birdstar.BirdexEvent
|
||||
import com.vitorpamplona.quartz.experimental.bounties.bountyBaseReward
|
||||
import com.vitorpamplona.quartz.experimental.edits.TextNoteModificationEvent
|
||||
@@ -972,6 +974,8 @@ private fun FullBleedNoteCompose(
|
||||
RenderFundraiser(baseNote, makeItShort = false, accountViewModel, nav)
|
||||
} else if (noteEvent is BirdexEvent) {
|
||||
RenderBirdex(baseNote)
|
||||
} else if (noteEvent is BirdDetectionEvent) {
|
||||
RenderBirdDetection(baseNote)
|
||||
} else if (noteEvent is RoadEventReportEvent) {
|
||||
RenderRoadEventReport(baseNote)
|
||||
} else if (noteEvent is RoadEventConfirmationEvent) {
|
||||
|
||||
@@ -3485,6 +3485,7 @@
|
||||
<string name="goal_progress">%1$s funded of %2$s sats goal</string>
|
||||
<string name="fundraiser_ends">Ends %1$s</string>
|
||||
<string name="fundraiser_onchain_donation">On-chain donation</string>
|
||||
<string name="bird_detection_title">Bird detection</string>
|
||||
<plurals name="birdex_species_count">
|
||||
<item quantity="one">Birdex · %1$d species</item>
|
||||
<item quantity="other">Birdex · %1$d species</item>
|
||||
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* 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.experimental.birdstar
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.firstTagValue
|
||||
import com.vitorpamplona.quartz.nip31Alts.alt
|
||||
import com.vitorpamplona.quartz.nip50Search.SearchableEvent
|
||||
|
||||
/**
|
||||
* Birdstar bird detection (kind 2473).
|
||||
*
|
||||
* An app-specific **regular** kind published by the Birdstar app
|
||||
* (`birdstar.app`) — a single bird sighting/detection. It is **not** defined by
|
||||
* any NIP; the schema below is derived from events seen in the wild. It is the
|
||||
* per-sighting companion of the replaceable [BirdexEvent] (kind 12473) life list.
|
||||
*
|
||||
* The event carries no body — its payload is in the tags:
|
||||
*
|
||||
* - `n` — the detected species' scientific name (e.g. `Porphyrio martinica`).
|
||||
* - `i` — an external identity reference for the species, a Wikidata entity URL
|
||||
* (NIP-73 style, e.g. `https://www.wikidata.org/entity/Q27074644`).
|
||||
* - `g` — a geohash of the detection location (standard NIP-01 `g` tag; use
|
||||
* `Event.geohashes()` to read it).
|
||||
* - `alt` — a human-readable summary written by the publisher
|
||||
* (e.g. `Bird detection: Purple Gallinule (Porphyrio martinica)`).
|
||||
*/
|
||||
@Immutable
|
||||
class BirdDetectionEvent(
|
||||
id: HexKey,
|
||||
pubKey: HexKey,
|
||||
createdAt: Long,
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : Event(id, pubKey, createdAt, KIND, tags, content, sig),
|
||||
SearchableEvent {
|
||||
override fun indexableContent() = listOfNotNull(summary(), speciesName()).joinToString("\n")
|
||||
|
||||
/** Scientific name of the detected species, from the `n` tag (may be null). */
|
||||
fun speciesName() = tags.firstTagValue("n")
|
||||
|
||||
/**
|
||||
* External species reference (Wikidata entity URL), from the `i` tag.
|
||||
* Only http(s) URLs are returned — UIs render this as a clickable link,
|
||||
* so other schemes are rejected here rather than at every call site.
|
||||
*/
|
||||
fun speciesReference() = tags.firstTagValue("i")?.takeIf { it.startsWith("https://") || it.startsWith("http://") }
|
||||
|
||||
/** Publisher-provided human-readable summary, from the NIP-31 `alt` tag (may be null). */
|
||||
fun summary() = tags.alt()
|
||||
|
||||
/**
|
||||
* Common (vernacular) species name, parsed out of the `alt` tag. Birdstar
|
||||
* currently writes `Bird detection: <Common name> (<Scientific name>)` (in
|
||||
* English); this strips the fixed prefix and the trailing parenthetical.
|
||||
* Null when the `alt` tag is missing or nothing is left after stripping —
|
||||
* including if a future Birdstar release rewords the alt text.
|
||||
*/
|
||||
fun commonName(): String? {
|
||||
val alt = summary() ?: return null
|
||||
if (!alt.startsWith(ALT_PREFIX)) return null
|
||||
return alt
|
||||
.removePrefix(ALT_PREFIX)
|
||||
.substringBeforeLast(" (")
|
||||
.trim()
|
||||
.ifBlank { null }
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val KIND = 2473
|
||||
private const val ALT_PREFIX = "Bird detection:"
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -23,8 +23,8 @@ package com.vitorpamplona.quartz.experimental.birdstar
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.nip01Core.core.BaseReplaceableEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.firstTagValue
|
||||
import com.vitorpamplona.quartz.nip01Core.core.mapValueTagged
|
||||
import com.vitorpamplona.quartz.nip31Alts.alt
|
||||
import com.vitorpamplona.quartz.nip50Search.SearchableEvent
|
||||
|
||||
/**
|
||||
@@ -65,8 +65,8 @@ class BirdexEvent(
|
||||
/** Number of collected species (one `n` tag per species). */
|
||||
fun speciesCount() = speciesNames().size
|
||||
|
||||
/** Publisher-provided human-readable summary, from the `alt` tag (may be null). */
|
||||
fun summary() = tags.firstTagValue("alt")
|
||||
/** Publisher-provided human-readable summary, from the NIP-31 `alt` tag (may be null). */
|
||||
fun summary() = tags.alt()
|
||||
|
||||
companion object {
|
||||
const val KIND = 12473
|
||||
|
||||
@@ -27,6 +27,7 @@ import com.vitorpamplona.quartz.experimental.attestations.recommendation.Attesto
|
||||
import com.vitorpamplona.quartz.experimental.attestations.request.AttestationRequestEvent
|
||||
import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent
|
||||
import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent
|
||||
import com.vitorpamplona.quartz.experimental.birdstar.BirdDetectionEvent
|
||||
import com.vitorpamplona.quartz.experimental.birdstar.BirdexEvent
|
||||
import com.vitorpamplona.quartz.experimental.clink.debits.DebitEvent
|
||||
import com.vitorpamplona.quartz.experimental.clink.manage.ManageEvent
|
||||
@@ -558,6 +559,7 @@ object KindNames {
|
||||
GoodWikiRelayListEvent.KIND to KindName("Wiki Relays", "51"),
|
||||
UserGraspListEvent.KIND to KindName("GRASP Servers", "34"),
|
||||
BirdexEvent.KIND to KindName("Birdex", null),
|
||||
BirdDetectionEvent.KIND to KindName("Bird Detection", null),
|
||||
NwcInfoEvent.KIND to KindName("NWC Info", "47"),
|
||||
RelayMembershipListEvent.KIND to KindName("Relay Memberships", "43"),
|
||||
RootSiteEvent.KIND to KindName("Website Root", "5A"),
|
||||
|
||||
@@ -29,6 +29,7 @@ import com.vitorpamplona.quartz.experimental.attestations.recommendation.Attesto
|
||||
import com.vitorpamplona.quartz.experimental.attestations.request.AttestationRequestEvent
|
||||
import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent
|
||||
import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent
|
||||
import com.vitorpamplona.quartz.experimental.birdstar.BirdDetectionEvent
|
||||
import com.vitorpamplona.quartz.experimental.birdstar.BirdexEvent
|
||||
import com.vitorpamplona.quartz.experimental.clink.debits.DebitEvent
|
||||
import com.vitorpamplona.quartz.experimental.clink.manage.ManageEvent
|
||||
@@ -354,6 +355,7 @@ class EventFactory {
|
||||
AudioTrackEvent.KIND -> AudioTrackEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
BadgeAwardEvent.KIND -> BadgeAwardEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
BadgeDefinitionEvent.KIND -> BadgeDefinitionEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
BirdDetectionEvent.KIND -> BirdDetectionEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
BirdexEvent.KIND -> BirdexEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
BidEvent.KIND -> BidEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
BidConfirmationEvent.KIND -> BidConfirmationEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
/*
|
||||
* 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.experimental.birdstar
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.geohash.geohashes
|
||||
import com.vitorpamplona.quartz.utils.EventFactory
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertIs
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class BirdDetectionEventTest {
|
||||
// Real event from the wild (relay.ditto.pub), published by birdstar.app
|
||||
private fun sampleEvent(): Event =
|
||||
EventFactory.create(
|
||||
id = "2b1450b8886c997ff14ab57d46fc5a60929ab8bba3dc0c7f1ebfd5916e41a0cc",
|
||||
pubKey = "0461fcbecc4c3374439932d6b8f11269ccdb7cc973ad7a50ae362db135a474dd",
|
||||
createdAt = 1_783_089_908L,
|
||||
kind = BirdDetectionEvent.KIND,
|
||||
tags =
|
||||
arrayOf(
|
||||
arrayOf("alt", "Bird detection: Purple Gallinule (Porphyrio martinica)"),
|
||||
arrayOf("i", "https://www.wikidata.org/entity/Q27074644"),
|
||||
arrayOf("n", "Porphyrio martinica"),
|
||||
arrayOf("g", "9vk"),
|
||||
arrayOf("client", "birdstar.app"),
|
||||
),
|
||||
content = "",
|
||||
sig =
|
||||
"7a30bea2cb4b5b2a826448294bfae072d2948f02e3d259f824c7706fbc1e2a32" +
|
||||
"5661efef9a63d5127e7fc26abbd15df55b98df9f44017f19de91616e97b5cad7",
|
||||
)
|
||||
|
||||
@Test
|
||||
fun factoryBuildsBirdDetectionForKind2473() {
|
||||
assertIs<BirdDetectionEvent>(sampleEvent())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun speciesReferenceRejectsNonHttpValues() {
|
||||
val event: Event =
|
||||
EventFactory.create(
|
||||
id = "00".repeat(32),
|
||||
pubKey = "00".repeat(32),
|
||||
createdAt = 1_783_089_908L,
|
||||
kind = BirdDetectionEvent.KIND,
|
||||
tags = arrayOf(arrayOf("i", "javascript:alert(1)")),
|
||||
content = "",
|
||||
sig = "00".repeat(64),
|
||||
)
|
||||
assertIs<BirdDetectionEvent>(event)
|
||||
|
||||
assertEquals(null, event.speciesReference())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun kind2473IsNowKnown() {
|
||||
assertTrue(EventFactory.isKnownKind(BirdDetectionEvent.KIND), "kind 2473 should be a known kind")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parsesCommonNameFromAltTag() {
|
||||
val event = sampleEvent()
|
||||
assertIs<BirdDetectionEvent>(event)
|
||||
|
||||
assertEquals("Purple Gallinule", event.commonName())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun commonNameIsNullWithoutAltTag() {
|
||||
val event: Event =
|
||||
EventFactory.create(
|
||||
id = "00".repeat(32),
|
||||
pubKey = "00".repeat(32),
|
||||
createdAt = 1_783_089_908L,
|
||||
kind = BirdDetectionEvent.KIND,
|
||||
tags = arrayOf(arrayOf("n", "Porphyrio martinica")),
|
||||
content = "",
|
||||
sig = "00".repeat(64),
|
||||
)
|
||||
assertIs<BirdDetectionEvent>(event)
|
||||
|
||||
assertEquals(null, event.commonName())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun commonNameIsNullWhenAltLacksTheBirdstarPrefix() {
|
||||
val event: Event =
|
||||
EventFactory.create(
|
||||
id = "00".repeat(32),
|
||||
pubKey = "00".repeat(32),
|
||||
createdAt = 1_783_089_908L,
|
||||
kind = BirdDetectionEvent.KIND,
|
||||
tags = arrayOf(arrayOf("alt", "Arbitrary attacker-controlled title (spoof)")),
|
||||
content = "",
|
||||
sig = "00".repeat(64),
|
||||
)
|
||||
assertIs<BirdDetectionEvent>(event)
|
||||
|
||||
assertEquals(null, event.commonName())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun commonNameHandlesAltWithoutParenthetical() {
|
||||
val event: Event =
|
||||
EventFactory.create(
|
||||
id = "00".repeat(32),
|
||||
pubKey = "00".repeat(32),
|
||||
createdAt = 1_783_089_908L,
|
||||
kind = BirdDetectionEvent.KIND,
|
||||
tags = arrayOf(arrayOf("alt", "Bird detection: Purple Gallinule")),
|
||||
content = "",
|
||||
sig = "00".repeat(64),
|
||||
)
|
||||
assertIs<BirdDetectionEvent>(event)
|
||||
|
||||
assertEquals("Purple Gallinule", event.commonName())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parsesBirdDetectionFields() {
|
||||
val event = sampleEvent()
|
||||
assertIs<BirdDetectionEvent>(event)
|
||||
|
||||
assertEquals("Porphyrio martinica", event.speciesName())
|
||||
assertEquals("https://www.wikidata.org/entity/Q27074644", event.speciesReference())
|
||||
assertEquals("Bird detection: Purple Gallinule (Porphyrio martinica)", event.summary())
|
||||
assertEquals(listOf("9vk"), event.geohashes())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user