refactor(roadstr): match NIP-88 polls architecture; register in LocalCache

Restructure the experimental roadstr events to mirror the nip88Polls
file layout, and wire the kinds into LocalCache (previously unhandled,
so they hit the "Event Not Supported" fallback and were dropped).

- Split into report/ and confirmation/ sub-packages, each with
  XEvent.kt + TagArrayBuilderExt.kt + TagArrayExt.kt + tags/, matching
  nip88Polls/{poll,response}. build() now uses eventTemplate + typed
  builder-ext functions; accessors delegate to TagArray ext functions.
- Bundle each enum into its tag class (RoadEventType in RoadEventTypeTag,
  RoadEventStatus in RoadEventStatusTag), like PollType in PollTypeTag.
- Add RoadReportTag (a GenericETag `e`-reference) mirroring PollTag, and
  have RoadEventConfirmationEvent implement EventHintProvider so the
  referenced report is linkable — parallel to PollResponseEvent.
- Shared lat/lon/geohash tag codecs live in roadstr/tags/ with generic
  builder/parse extensions.
- LocalCache: consume kinds 1315/1316 as regular events.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cn4Lk53CcXqYFYHMut4YfY
This commit is contained in:
Claude
2026-06-18 20:57:41 +00:00
parent 61255198b3
commit 4662cb75d0
17 changed files with 400 additions and 86 deletions
@@ -75,6 +75,8 @@ import com.vitorpamplona.quartz.experimental.nipsOnNostr.NipTextEvent
import com.vitorpamplona.quartz.experimental.nns.NNSEvent
import com.vitorpamplona.quartz.experimental.notifications.wake.WakeUpEvent
import com.vitorpamplona.quartz.experimental.profileGallery.ProfileGalleryEntryEvent
import com.vitorpamplona.quartz.experimental.roadstr.confirmation.RoadEventConfirmationEvent
import com.vitorpamplona.quartz.experimental.roadstr.report.RoadEventReportEvent
import com.vitorpamplona.quartz.experimental.zapPolls.ZapPollEvent
import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageEvent
import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageRelayListEvent
@@ -3870,6 +3872,14 @@ object LocalCache : ILocalCache, ICacheProvider {
consume(event, relay, wasVerified)
}
is RoadEventReportEvent -> {
consumeRegularEvent(event, relay, wasVerified)
}
is RoadEventConfirmationEvent -> {
consumeRegularEvent(event, relay, wasVerified)
}
is RelayDiscoveryEvent -> {
consumeBaseReplaceable(event, relay, wasVerified)
}
@@ -237,8 +237,8 @@ import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.asset.SoftwareAss
import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.release.isNip82SoftwareRelease
import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent
import com.vitorpamplona.quartz.experimental.nipsOnNostr.NipTextEvent
import com.vitorpamplona.quartz.experimental.roadstr.RoadEventConfirmationEvent
import com.vitorpamplona.quartz.experimental.roadstr.RoadEventReportEvent
import com.vitorpamplona.quartz.experimental.roadstr.confirmation.RoadEventConfirmationEvent
import com.vitorpamplona.quartz.experimental.roadstr.report.RoadEventReportEvent
import com.vitorpamplona.quartz.experimental.zapPolls.ZapPollEvent
import com.vitorpamplona.quartz.nip01Core.tags.geohash.geoHashOrScope
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
@@ -40,10 +40,10 @@ import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.note.creators.location.LoadCityName
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import com.vitorpamplona.amethyst.ui.theme.replyModifier
import com.vitorpamplona.quartz.experimental.roadstr.RoadEventConfirmationEvent
import com.vitorpamplona.quartz.experimental.roadstr.RoadEventReportEvent
import com.vitorpamplona.quartz.experimental.roadstr.RoadEventStatus
import com.vitorpamplona.quartz.experimental.roadstr.RoadEventType
import com.vitorpamplona.quartz.experimental.roadstr.confirmation.RoadEventConfirmationEvent
import com.vitorpamplona.quartz.experimental.roadstr.confirmation.tags.RoadEventStatus
import com.vitorpamplona.quartz.experimental.roadstr.report.RoadEventReportEvent
import com.vitorpamplona.quartz.experimental.roadstr.report.tags.RoadEventType
/** Emoji marker for a road event category — keeps the card icon-light and locale-independent. */
private fun RoadEventType.emoji(): String =
@@ -252,8 +252,8 @@ import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.application.Softw
import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.asset.SoftwareAssetEvent
import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.release.isNip82SoftwareRelease
import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent
import com.vitorpamplona.quartz.experimental.roadstr.RoadEventConfirmationEvent
import com.vitorpamplona.quartz.experimental.roadstr.RoadEventReportEvent
import com.vitorpamplona.quartz.experimental.roadstr.confirmation.RoadEventConfirmationEvent
import com.vitorpamplona.quartz.experimental.roadstr.report.RoadEventReportEvent
import com.vitorpamplona.quartz.experimental.zapPolls.ZapPollEvent
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.tags.geohash.geoHashOrScope
@@ -18,24 +18,25 @@
* 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.roadstr
package com.vitorpamplona.quartz.experimental.roadstr.confirmation
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.experimental.roadstr.tags.LatitudeTag
import com.vitorpamplona.quartz.experimental.roadstr.tags.LongitudeTag
import com.vitorpamplona.quartz.experimental.roadstr.tags.RoadEventStatusTag
import com.vitorpamplona.quartz.experimental.roadstr.confirmation.tags.RoadEventStatus
import com.vitorpamplona.quartz.experimental.roadstr.confirmation.tags.RoadReportTag
import com.vitorpamplona.quartz.experimental.roadstr.report.RoadEventReportEvent
import com.vitorpamplona.quartz.experimental.roadstr.tags.coordinates
import com.vitorpamplona.quartz.experimental.roadstr.tags.latitude
import com.vitorpamplona.quartz.experimental.roadstr.tags.longitude
import com.vitorpamplona.quartz.experimental.roadstr.tags.roadGeohashes
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate
import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle
import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
import com.vitorpamplona.quartz.nip01Core.tags.events.ETag
import com.vitorpamplona.quartz.nip01Core.tags.geohash.GeoHash
import com.vitorpamplona.quartz.nip01Core.tags.geohash.GeoHashTag
import com.vitorpamplona.quartz.nip01Core.tags.geohash.geohashes
import com.vitorpamplona.quartz.nip31Alts.AltTag
import com.vitorpamplona.quartz.nip31Alts.alt
import com.vitorpamplona.quartz.nip40Expiration.ExpirationTag
import com.vitorpamplona.quartz.nip40Expiration.expiration
import com.vitorpamplona.quartz.utils.TimeUtils
@@ -46,7 +47,7 @@ import com.vitorpamplona.quartz.utils.TimeUtils
* <https://github.com/jooray/roadstr>.
*
* Tags (canonical order):
* - `e` — the referenced report event id.
* - `e` — the referenced report event id ([RoadReportTag]).
* - `status` — [RoadEventStatus.STILL_THERE] (confirm) or [RoadEventStatus.NO_LONGER_THERE] (deny).
* - `g` / `lat` / `lon` — optional copy of the report's location, so confirmations
* are reachable through the same `#g` geohash queries as reports.
@@ -61,11 +62,16 @@ class RoadEventConfirmationEvent(
tags: Array<Array<String>>,
content: String,
sig: HexKey,
) : Event(id, pubKey, createdAt, KIND, tags, content, sig) {
/** The referenced report event id from the first `e` tag. */
fun reportId() = tags.firstNotNullOfOrNull(ETag::parse)?.eventId
) : Event(id, pubKey, createdAt, KIND, tags, content, sig),
EventHintProvider {
override fun eventHints() = tags.mapNotNull(RoadReportTag::parseAsHint)
fun status() = tags.firstNotNullOfOrNull(RoadEventStatusTag::parse)
override fun linkedEventIds() = tags.mapNotNull(RoadReportTag::parseId)
/** The referenced report event id from the first `e` tag. */
fun reportId() = tags.roadReport()?.eventId
fun status() = tags.roadEventStatus()
fun isConfirmation() = status() == RoadEventStatus.STILL_THERE
@@ -73,13 +79,13 @@ class RoadEventConfirmationEvent(
fun geohashes() = tags.geohashes()
fun latitude() = tags.firstNotNullOfOrNull(LatitudeTag::parse)
fun latitude() = tags.latitude()
fun longitude() = tags.firstNotNullOfOrNull(LongitudeTag::parse)
fun longitude() = tags.longitude()
fun alt() = tags.firstNotNullOfOrNull(AltTag::parse)
fun relayExpiration() = tags.firstNotNullOfOrNull(ExpirationTag::parse)
fun relayExpiration() = tags.expiration()
companion object {
const val KIND = 1316
@@ -92,21 +98,28 @@ class RoadEventConfirmationEvent(
longitude: Double? = null,
createdAt: Long = TimeUtils.now(),
initializer: TagArrayBuilder<RoadEventConfirmationEvent>.() -> Unit = {},
): EventTemplate<RoadEventConfirmationEvent> =
eventTemplate(KIND, "", createdAt) {
add(ETag.assemble(reportId, null, null))
add(RoadEventStatusTag.assemble(status))
if (latitude != null && longitude != null) {
val geohash6 = GeoHash.encode(latitude, longitude, 6).toString()
add(GeoHashTag.assembleSingle(geohash6.substring(0, 4)))
add(GeoHashTag.assembleSingle(geohash6.substring(0, 5)))
add(GeoHashTag.assembleSingle(geohash6))
add(LatitudeTag.assemble(latitude))
add(LongitudeTag.assemble(longitude))
}
expiration(createdAt + RoadEventReportEvent.RELAY_TTL_SECONDS)
alt(ALT_DESCRIPTION)
initializer()
) = eventTemplate(KIND, "", createdAt) {
report(reportId)
status(status)
if (latitude != null && longitude != null) {
roadGeohashes(latitude, longitude)
coordinates(latitude, longitude)
}
expiration(createdAt + RoadEventReportEvent.RELAY_TTL_SECONDS)
alt(ALT_DESCRIPTION)
initializer()
}
fun build(
report: EventHintBundle<RoadEventReportEvent>,
status: RoadEventStatus,
latitude: Double? = null,
longitude: Double? = null,
createdAt: Long = TimeUtils.now(),
initializer: TagArrayBuilder<RoadEventConfirmationEvent>.() -> Unit = {},
) = build(report.event.id, status, latitude, longitude, createdAt) {
report(report)
initializer()
}
}
}
@@ -0,0 +1,35 @@
/*
* 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.roadstr.confirmation
import com.vitorpamplona.quartz.experimental.roadstr.confirmation.tags.RoadEventStatus
import com.vitorpamplona.quartz.experimental.roadstr.confirmation.tags.RoadEventStatusTag
import com.vitorpamplona.quartz.experimental.roadstr.confirmation.tags.RoadReportTag
import com.vitorpamplona.quartz.experimental.roadstr.report.RoadEventReportEvent
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle
fun TagArrayBuilder<RoadEventConfirmationEvent>.report(reportId: HexKey) = addUnique(RoadReportTag.assemble(reportId, null, null))
fun TagArrayBuilder<RoadEventConfirmationEvent>.report(reportHint: EventHintBundle<RoadEventReportEvent>) = addUnique(RoadReportTag.assemble(reportHint))
fun TagArrayBuilder<RoadEventConfirmationEvent>.status(status: RoadEventStatus) = addUnique(RoadEventStatusTag.assemble(status))
@@ -0,0 +1,29 @@
/*
* 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.roadstr.confirmation
import com.vitorpamplona.quartz.experimental.roadstr.confirmation.tags.RoadEventStatusTag
import com.vitorpamplona.quartz.experimental.roadstr.confirmation.tags.RoadReportTag
import com.vitorpamplona.quartz.nip01Core.core.TagArray
fun TagArray.roadReport() = firstNotNullOfOrNull(RoadReportTag::parse)
fun TagArray.roadEventStatus() = firstNotNullOfOrNull(RoadEventStatusTag::parse)
@@ -18,12 +18,24 @@
* 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.roadstr.tags
package com.vitorpamplona.quartz.experimental.roadstr.confirmation.tags
import com.vitorpamplona.quartz.experimental.roadstr.RoadEventStatus
import com.vitorpamplona.quartz.nip01Core.core.has
import com.vitorpamplona.quartz.utils.ensure
/** Confirmation status carried in the `status` tag of a Roadstr confirmation (kind 1316). */
enum class RoadEventStatus(
val code: String,
) {
STILL_THERE("still_there"),
NO_LONGER_THERE("no_longer_there"),
;
companion object {
fun fromCode(code: String?): RoadEventStatus? = entries.firstOrNull { it.code == code }
}
}
/** The `status` tag of a Roadstr confirmation (kind 1316): `still_there` or `no_longer_there`. */
class RoadEventStatusTag {
companion object {
@@ -0,0 +1,96 @@
/*
* 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.roadstr.confirmation.tags
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.experimental.roadstr.report.RoadEventReportEvent
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.has
import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle
import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHint
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
import com.vitorpamplona.quartz.nip01Core.tags.events.GenericETag
import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent
import com.vitorpamplona.quartz.utils.arrayOfNotNull
import com.vitorpamplona.quartz.utils.ensure
/** The `e` tag of a Roadstr confirmation (kind 1316): the referenced report event. */
@Immutable
data class RoadReportTag(
override val eventId: HexKey,
) : GenericETag {
override var relay: NormalizedRelayUrl? = null
override var author: HexKey? = null
constructor(eventId: HexKey, relayHint: NormalizedRelayUrl? = null, authorPubKeyHex: HexKey? = null) : this(eventId) {
this.relay = relayHint
this.author = authorPubKeyHex
}
fun toNEvent(): String = NEvent.create(eventId, author, RoadEventReportEvent.KIND, relay)
override fun toTagArray() = assemble(eventId, relay, author)
companion object {
const val TAG_NAME = "e"
fun isTagged(tag: Array<String>) = tag.has(1) && tag[0] == TAG_NAME && tag[1].length == 64
fun parse(tag: Array<String>): RoadReportTag? {
ensure(tag.has(1)) { return null }
ensure(tag[0] == TAG_NAME) { return null }
ensure(tag[1].length == 64) { return null }
val hint = tag.getOrNull(2)?.let { RelayUrlNormalizer.normalizeOrNull(it) }
return RoadReportTag(tag[1], hint, tag.getOrNull(3))
}
fun parseId(tag: Array<String>): String? {
ensure(tag.has(1)) { return null }
ensure(tag[0] == TAG_NAME) { return null }
ensure(tag[1].length == 64) { return null }
return tag[1]
}
fun parseAsHint(tag: Array<String>): EventIdHint? {
ensure(tag.has(2)) { return null }
ensure(tag[0] == TAG_NAME) { return null }
ensure(tag[1].length == 64) { return null }
ensure(tag[2].isNotEmpty()) { return null }
val hint = RelayUrlNormalizer.normalizeOrNull(tag[2])
ensure(hint != null) { return null }
return EventIdHint(tag[1], hint)
}
fun assemble(
eventId: HexKey,
relay: NormalizedRelayUrl?,
author: HexKey?,
) = arrayOfNotNull(TAG_NAME, eventId, relay?.url, author)
fun assemble(eventHint: EventHintBundle<RoadEventReportEvent>) = assemble(eventHint.event.id, eventHint.relay, eventHint.event.pubKey)
}
}
@@ -18,23 +18,21 @@
* 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.roadstr
package com.vitorpamplona.quartz.experimental.roadstr.report
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.experimental.roadstr.tags.LatitudeTag
import com.vitorpamplona.quartz.experimental.roadstr.tags.LongitudeTag
import com.vitorpamplona.quartz.experimental.roadstr.report.tags.RoadEventType
import com.vitorpamplona.quartz.experimental.roadstr.tags.coordinates
import com.vitorpamplona.quartz.experimental.roadstr.tags.latitude
import com.vitorpamplona.quartz.experimental.roadstr.tags.longitude
import com.vitorpamplona.quartz.experimental.roadstr.tags.roadGeohashes
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
import com.vitorpamplona.quartz.nip01Core.tags.geohash.GeoHash
import com.vitorpamplona.quartz.nip01Core.tags.geohash.GeoHashTag
import com.vitorpamplona.quartz.nip01Core.tags.geohash.geohashes
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.HashtagTag
import com.vitorpamplona.quartz.nip31Alts.AltTag
import com.vitorpamplona.quartz.nip31Alts.alt
import com.vitorpamplona.quartz.nip40Expiration.ExpirationTag
import com.vitorpamplona.quartz.nip40Expiration.expiration
import com.vitorpamplona.quartz.utils.TimeUtils
@@ -64,22 +62,22 @@ class RoadEventReportEvent(
content: String,
sig: HexKey,
) : Event(id, pubKey, createdAt, KIND, tags, content, sig) {
/** The raw event-type code from the first `t` tag (may be a value outside [RoadEventType]). */
fun roadEventTypeCode() = tags.firstNotNullOfOrNull(HashtagTag::parse)
/** The parsed [RoadEventType], or null when the `t` tag is missing/unknown. */
fun roadEventType() = RoadEventType.fromCode(roadEventTypeCode())
fun roadEventType() = tags.roadEventType()
/** All `g` geohash tags, finest-to-coarsest as published. */
/** The raw event-type code from the first `t` tag (may be outside [RoadEventType]). */
fun roadEventTypeCode() = tags.roadEventTypeCode()
/** All `g` geohash tags, coarse-to-fine as published. */
fun geohashes() = tags.geohashes()
fun latitude() = tags.firstNotNullOfOrNull(LatitudeTag::parse)
fun latitude() = tags.latitude()
fun longitude() = tags.firstNotNullOfOrNull(LongitudeTag::parse)
fun longitude() = tags.longitude()
fun alt() = tags.firstNotNullOfOrNull(AltTag::parse)
fun relayExpiration() = tags.firstNotNullOfOrNull(ExpirationTag::parse)
fun relayExpiration() = tags.expiration()
/** Client-side effective expiry (`created_at + ` the type's TTL), or null if the type is unknown. */
fun effectiveExpirationAt(): Long? = roadEventType()?.let { createdAt + it.effectiveTtlSeconds }
@@ -103,19 +101,13 @@ class RoadEventReportEvent(
comment: String = "",
createdAt: Long = TimeUtils.now(),
initializer: TagArrayBuilder<RoadEventReportEvent>.() -> Unit = {},
): EventTemplate<RoadEventReportEvent> {
val geohash6 = GeoHash.encode(latitude, longitude, 6).toString()
return eventTemplate(KIND, comment, createdAt) {
add(HashtagTag.assemble(type.code))
add(GeoHashTag.assembleSingle(geohash6.substring(0, 4)))
add(GeoHashTag.assembleSingle(geohash6.substring(0, 5)))
add(GeoHashTag.assembleSingle(geohash6))
add(LatitudeTag.assemble(latitude))
add(LongitudeTag.assemble(longitude))
expiration(createdAt + RELAY_TTL_SECONDS)
alt(altDescription(type))
initializer()
}
) = eventTemplate(KIND, comment, createdAt) {
roadEventType(type)
roadGeohashes(latitude, longitude)
coordinates(latitude, longitude)
expiration(createdAt + RELAY_TTL_SECONDS)
alt(altDescription(type))
initializer()
}
}
}
@@ -18,17 +18,10 @@
* 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.roadstr
package com.vitorpamplona.quartz.experimental.roadstr.report
/** Confirmation status carried in the `status` tag of a Roadstr confirmation (kind 1316). */
enum class RoadEventStatus(
val code: String,
) {
STILL_THERE("still_there"),
NO_LONGER_THERE("no_longer_there"),
;
import com.vitorpamplona.quartz.experimental.roadstr.report.tags.RoadEventType
import com.vitorpamplona.quartz.experimental.roadstr.report.tags.RoadEventTypeTag
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
companion object {
fun fromCode(code: String?): RoadEventStatus? = entries.firstOrNull { it.code == code }
}
}
fun TagArrayBuilder<RoadEventReportEvent>.roadEventType(type: RoadEventType) = add(RoadEventTypeTag.assemble(type))
@@ -0,0 +1,28 @@
/*
* 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.roadstr.report
import com.vitorpamplona.quartz.experimental.roadstr.report.tags.RoadEventTypeTag
import com.vitorpamplona.quartz.nip01Core.core.TagArray
fun TagArray.roadEventType() = firstNotNullOfOrNull(RoadEventTypeTag::parse)
fun TagArray.roadEventTypeCode() = firstNotNullOfOrNull(RoadEventTypeTag::parseCode)
@@ -18,7 +18,10 @@
* 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.roadstr
package com.vitorpamplona.quartz.experimental.roadstr.report.tags
import com.vitorpamplona.quartz.nip01Core.core.has
import com.vitorpamplona.quartz.utils.ensure
/**
* Road event categories carried in the `t` tag of a Roadstr report (kind 1315).
@@ -51,3 +54,29 @@ enum class RoadEventType(
fun fromCode(code: String?): RoadEventType? = entries.firstOrNull { it.code == code }
}
}
/** The `t` tag of a Roadstr report (kind 1315): the [RoadEventType] code. */
class RoadEventTypeTag {
companion object {
const val TAG_NAME = "t"
fun isTag(tag: Array<String>) = tag.has(1) && tag[0] == TAG_NAME && tag[1].isNotEmpty()
/** Parses the tag into a known [RoadEventType], or null when missing/unknown. */
fun parse(tag: Array<String>): RoadEventType? {
ensure(tag.has(1)) { return null }
ensure(tag[0] == TAG_NAME) { return null }
return RoadEventType.fromCode(tag[1])
}
/** Returns the raw `t` value even when it is not a known [RoadEventType]. */
fun parseCode(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]
}
fun assemble(type: RoadEventType) = arrayOf(TAG_NAME, type.code)
}
}
@@ -0,0 +1,46 @@
/*
* 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.roadstr.tags
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
import com.vitorpamplona.quartz.nip01Core.tags.geohash.GeoHash
import com.vitorpamplona.quartz.nip01Core.tags.geohash.GeoHashTag
/** Adds `lat`/`lon` tags (7-decimal strings) shared by Roadstr reports and confirmations. */
fun <T : Event> TagArrayBuilder<T>.coordinates(
latitude: Double,
longitude: Double,
) = apply {
add(LatitudeTag.assemble(latitude))
add(LongitudeTag.assemble(longitude))
}
/** Adds the three `g` geohash tags (precision 4, 5 and 6) derived from [latitude]/[longitude]. */
fun <T : Event> TagArrayBuilder<T>.roadGeohashes(
latitude: Double,
longitude: Double,
) = apply {
val geohash6 = GeoHash.encode(latitude, longitude, 6).toString()
add(GeoHashTag.assembleSingle(geohash6.substring(0, 4)))
add(GeoHashTag.assembleSingle(geohash6.substring(0, 5)))
add(GeoHashTag.assembleSingle(geohash6))
}
@@ -0,0 +1,27 @@
/*
* 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.roadstr.tags
import com.vitorpamplona.quartz.nip01Core.core.TagArray
fun TagArray.latitude() = firstNotNullOfOrNull(LatitudeTag::parse)
fun TagArray.longitude() = firstNotNullOfOrNull(LongitudeTag::parse)
@@ -53,8 +53,8 @@ import com.vitorpamplona.quartz.experimental.nipsOnNostr.NipTextEvent
import com.vitorpamplona.quartz.experimental.nns.NNSEvent
import com.vitorpamplona.quartz.experimental.notifications.wake.WakeUpEvent
import com.vitorpamplona.quartz.experimental.profileGallery.ProfileGalleryEntryEvent
import com.vitorpamplona.quartz.experimental.roadstr.RoadEventConfirmationEvent
import com.vitorpamplona.quartz.experimental.roadstr.RoadEventReportEvent
import com.vitorpamplona.quartz.experimental.roadstr.confirmation.RoadEventConfirmationEvent
import com.vitorpamplona.quartz.experimental.roadstr.report.RoadEventReportEvent
import com.vitorpamplona.quartz.experimental.zapPolls.ZapPollEvent
import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageEvent
import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageRelayListEvent
@@ -20,6 +20,10 @@
*/
package com.vitorpamplona.quartz.experimental.roadstr
import com.vitorpamplona.quartz.experimental.roadstr.confirmation.RoadEventConfirmationEvent
import com.vitorpamplona.quartz.experimental.roadstr.confirmation.tags.RoadEventStatus
import com.vitorpamplona.quartz.experimental.roadstr.report.RoadEventReportEvent
import com.vitorpamplona.quartz.experimental.roadstr.report.tags.RoadEventType
import com.vitorpamplona.quartz.experimental.roadstr.tags.RoadCoordinate
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.tags.geohash.GeoHash