mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-05 19:28:25 +00:00
refactor(concord): chat plane reuses standard events + typed binding tags
First slice of the nip88Polls-structure refactor (plan in quartz/plans/
2026-07-11-concord-event-classes.md). A Concord chat rumor IS a standard Nostr
event plus a channel/epoch binding, so stop re-deriving it by kind number:
- New cord03Channels/tags/ChannelTag + EpochTag (typed, parse/assemble), plus
TagArrayBuilderExt (channel/epoch/channelBinding) and TagArrayExt
(concordChannel/concordEpoch/isConcordBoundTo) — the poll-package shape.
- ChannelChat.message/reply now build via ChatEvent.build{ channelBinding(...) }
and assemble the template into a rumor; reaction reuses ReactionEvent.KIND. The
minimal e/p/k reaction tags and q/p reply tags are kept byte-identical for Armada
interop (guarded by the Concord round-trip tests, which pass).
- Drop the ConcordKinds.MESSAGE/COMMENT/REACTION/DELETE aliases (they shadowed
ChatEvent/CommentEvent/ReactionEvent/DeletionEvent KINDs); callers use the real
event KINDs. VoicePresence uses the new tag classes instead of bindingTags.
Remaining Concord-specific kinds (control 3308, invites, guestbook, rekey, voice,
seals/wrap) get their own per-kind packages in later slices per the plan.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CzJ2Cwo8tg4oZq43oRa3ig
This commit is contained in:
+2
-1
@@ -40,6 +40,7 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nipC7Chats.ChatEvent
|
||||
|
||||
/** One decrypted, verified Concord channel message projected for display. */
|
||||
data class ConcordChatMessage(
|
||||
@@ -173,7 +174,7 @@ object ConcordActions {
|
||||
): List<ConcordChatMessage> =
|
||||
wraps
|
||||
.mapNotNull { wrap -> ConcordStreamEnvelope.openOrNull(wrap, channel)?.rumor }
|
||||
.filter { it.kind == ConcordKinds.MESSAGE && ChannelChat.isBoundTo(it, channelId, epoch) }
|
||||
.filter { it.kind == ChatEvent.KIND && ChannelChat.isBoundTo(it, channelId, epoch) }
|
||||
.map { ConcordChatMessage(it.id, it.pubKey, it.content, it.createdAt, channelId, epoch) }
|
||||
.sortedWith(compareBy({ it.createdAt }, { it.id }))
|
||||
|
||||
|
||||
+2
-2
@@ -23,10 +23,10 @@ package com.vitorpamplona.amethyst.commons.model.concord
|
||||
import com.vitorpamplona.amethyst.commons.actions.ConcordActions
|
||||
import com.vitorpamplona.quartz.concord.cord02Community.ConcordCommunityFactory
|
||||
import com.vitorpamplona.quartz.concord.cord02Community.ConcordCommunityListEntry
|
||||
import com.vitorpamplona.quartz.concord.events.ConcordKinds
|
||||
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
|
||||
import com.vitorpamplona.quartz.nipC7Chats.ChatEvent
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
@@ -73,7 +73,7 @@ class ConcordPlaneRegistryTest {
|
||||
assertNotNull(routedMsg)
|
||||
assertEquals(ConcordPlaneKind.CHANNEL, routedMsg.plane.kind)
|
||||
assertEquals(community.generalChannelIdHex, routedMsg.plane.channelId?.channelId)
|
||||
assertEquals(ConcordKinds.MESSAGE, routedMsg.opened.rumor.kind)
|
||||
assertEquals(ChatEvent.KIND, routedMsg.opened.rumor.kind)
|
||||
assertEquals("gm", routedMsg.opened.rumor.content)
|
||||
|
||||
// A wrap from an unrelated plane (different community) is not ours.
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
# Concord event layer → per-kind Event classes (nip88Polls structure)
|
||||
|
||||
## Why
|
||||
|
||||
The Concord quartz layer currently centralizes wire kinds in a single
|
||||
`concord/events/ConcordKinds.kt` constant object and hand-rolls rumors with raw
|
||||
string tags (see `cord03Channels/ChannelChat.kt`:
|
||||
`RumorAssembler.assembleRumor(kind = ConcordKinds.MESSAGE, tags = arrayOf(arrayOf("q", …)))`).
|
||||
Two problems:
|
||||
|
||||
1. **Duplicates standard Nostr kinds.** `ConcordKinds.MESSAGE=9`, `REACTION=7`,
|
||||
`DELETE=5`, `COMMENT=1111`, `EDIT=3302` shadow `ChatEvent.KIND`,
|
||||
`ReactionEvent.KIND`, `DeletionEvent.KIND`, `CommentEvent.KIND`. Concord chat
|
||||
rumors *are* those standard events (they already parse back as `ChatEvent`
|
||||
etc. on read) — the build side should reuse the classes, not re-derive by
|
||||
number.
|
||||
2. **No per-event structure.** Every other protocol in quartz gives each event
|
||||
kind a package: `XxxEvent.kt` (the `Event` subclass + `build`), plus
|
||||
`TagArrayBuilderExt.kt` / `TagArrayExt.kt` and a `tags/` folder of typed tag
|
||||
classes (`nip88Polls/poll/…` is the reference). Concord instead has loose
|
||||
builders (`ChannelChat`, `ControlEditionBuilder`) and stringly-typed tags.
|
||||
|
||||
Target: match the `nip88Polls` shape. Reuse standard events where the protocol
|
||||
uses standard kinds; give each genuinely-Concord kind its own package.
|
||||
|
||||
## Reuse standard events (chat plane, CORD-03)
|
||||
|
||||
A Concord chat rumor is a standard event + a `["channel", id]` + `["epoch", n]`
|
||||
binding. Introduce a binding-tag package and reuse the standard builders:
|
||||
|
||||
- `cord03Channels/tags/ChannelTag.kt`, `tags/EpochTag.kt` — typed tags.
|
||||
- `cord03Channels/TagArrayBuilderExt.kt` — `channel(id)`, `epoch(n)` on the DSL.
|
||||
- `cord03Channels/TagArrayExt.kt` — `channelId()`, `epoch()`, `isBoundTo(...)`.
|
||||
|
||||
| rumor | reuse | binding |
|
||||
|---|---|---|
|
||||
| message (9) | `ChatEvent.build` | `channel` + `epoch` |
|
||||
| reply (9 + q) | `ChatEvent.build` + `q`/`p` | `channel` + `epoch` |
|
||||
| reaction (7) | `ReactionEvent.build` | `channel` + `epoch` |
|
||||
| delete (5) | `DeletionEvent.build` | `channel` + `epoch` |
|
||||
|
||||
`ChannelChat` keeps its public API (returns unsigned rumors via `RumorAssembler`)
|
||||
but builds tags from the standard event's DSL + the binding ext. Delete
|
||||
`ConcordKinds.MESSAGE/REACTION/DELETE/COMMENT/EDIT`.
|
||||
|
||||
**Interop guard:** the exact on-wire tags must stay byte-identical to today's
|
||||
output (Armada compat). Keep `["q", parentId]` + `["p", parentAuthor]` for
|
||||
replies and `["e"/"p"/"k"]` for reactions — do not switch to `QEventTag`'s
|
||||
3-element form. Verify with `ConcordPlaneRegistryTest` + an amy↔Armada round-trip.
|
||||
|
||||
## New per-kind Event classes (genuinely Concord)
|
||||
|
||||
Each gets `concord/<cordXX>/<name>/XxxEvent.kt` + `TagArrayBuilderExt` +
|
||||
`TagArrayExt` + `tags/`:
|
||||
|
||||
| kind | event | CORD |
|
||||
|---|---|---|
|
||||
| 3308 | `ControlEditionEvent` (from `ControlEdition`/`ControlEditionBuilder`) | 02/04/06 |
|
||||
| 3303 | `RekeyEvent` | 06 |
|
||||
| 3306 / 3309 / 3312 | `GuestbookJoinLeaveEvent` / `KickEvent` / `SnapshotEvent` | 02 |
|
||||
| 3313 | `DirectInviteEvent` | 05 |
|
||||
| 23311 / 23313 | `TypingEvent` / `VoicePresenceEvent` | 03/07 |
|
||||
| 3310 | `WebxdcEvent` | 03 |
|
||||
| 33301 | `InviteBundleEvent` (from `ConcordInviteBundle`) | 05 |
|
||||
| 13303 | `InviteListEvent` | 05 |
|
||||
| 20013 / 20014 / 1059 / 21059 | envelope seals + inverted wrap | 01 |
|
||||
|
||||
`ConcordCommunityListEvent` (13302) is already an `Event` class (now a
|
||||
`BaseReplaceableEvent`) — keep, just move under a per-kind package if desired.
|
||||
|
||||
After the move, `ConcordKinds` retains only the truly-Concord kinds (envelope,
|
||||
control, rekey, guestbook, invites, voice), not the standard-Nostr aliases.
|
||||
|
||||
## Sequencing (incremental, compile + interop-test each)
|
||||
|
||||
1. Chat plane reuse (this doc's first section) — smallest blast radius (4 refs),
|
||||
highest clarity. **← start here.**
|
||||
2. Control plane 3308 → `ControlEditionEvent` package.
|
||||
3. Invites 33301 / 3313 / 13303.
|
||||
4. Guestbook + voice + rekey.
|
||||
5. Envelope seals/wrap.
|
||||
6. Shrink `ConcordKinds`, delete dead constants, update `EventFactory`.
|
||||
+39
-42
@@ -20,36 +20,31 @@
|
||||
*/
|
||||
package com.vitorpamplona.quartz.concord.cord03Channels
|
||||
|
||||
import com.vitorpamplona.quartz.concord.events.ConcordKinds
|
||||
import com.vitorpamplona.quartz.concord.cord03Channels.tags.ChannelTag
|
||||
import com.vitorpamplona.quartz.concord.cord03Channels.tags.EpochTag
|
||||
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.nip25Reactions.ReactionEvent
|
||||
import com.vitorpamplona.quartz.nip59Giftwrap.rumors.RumorAssembler
|
||||
import com.vitorpamplona.quartz.nipC7Chats.ChatEvent
|
||||
|
||||
/**
|
||||
* Chat Plane message binding (CORD-03).
|
||||
*
|
||||
* Every Chat Plane rumor — a message, reply, reaction, edit, or delete — commits
|
||||
* to the channel and epoch it belongs to via `["channel", <id>]` and
|
||||
* `["epoch", <n>]` tags inside the author-signed rumor. Recipients enforce this
|
||||
* binding ([isBoundTo]) so an event lifted from one channel/epoch can't be
|
||||
* replayed into another.
|
||||
* A Concord chat rumor **is** a standard Nostr event — a kind-9 [ChatEvent]
|
||||
* message/reply or a kind-7 [ReactionEvent] — that additionally commits to the
|
||||
* channel and epoch it belongs to via `["channel", <id>]` + `["epoch", <n>]` tags
|
||||
* (see [channel]/[epoch] and [ChannelTag]/[EpochTag]). This object reuses the
|
||||
* standard event builders and only adds the binding, so the same event classes
|
||||
* that render everywhere else in the app render Concord messages too. Recipients
|
||||
* enforce the binding ([TagArray.isConcordBoundTo]) so an event lifted from one
|
||||
* channel/epoch can't be replayed into another.
|
||||
*/
|
||||
object ChannelChat {
|
||||
const val TAG_CHANNEL = "channel"
|
||||
const val TAG_EPOCH = "epoch"
|
||||
|
||||
/** Builds the channel/epoch binding tags shared by every Chat Plane rumor. */
|
||||
fun bindingTags(
|
||||
channelId: HexKey,
|
||||
epoch: Long,
|
||||
): Array<Array<String>> = arrayOf(arrayOf(TAG_CHANNEL, channelId), arrayOf(TAG_EPOCH, epoch.toString()))
|
||||
|
||||
/**
|
||||
* Builds an unsigned kind-9 chat message rumor bound to [channelId]/[epoch].
|
||||
* Builds an unsigned kind-9 [ChatEvent] rumor bound to [channelId]/[epoch].
|
||||
* Wrap it for the channel plane with
|
||||
* [com.vitorpamplona.quartz.concord.envelope.ConcordStreamEnvelope] (encrypted
|
||||
* seal) to publish.
|
||||
* [com.vitorpamplona.quartz.concord.envelope.ConcordStreamEnvelope] to publish.
|
||||
*/
|
||||
fun message(
|
||||
authorPubKey: HexKey,
|
||||
@@ -60,11 +55,11 @@ object ChannelChat {
|
||||
extraTags: Array<Array<String>> = emptyArray(),
|
||||
): Event =
|
||||
RumorAssembler.assembleRumor(
|
||||
pubKey = authorPubKey,
|
||||
createdAt = createdAt,
|
||||
kind = ConcordKinds.MESSAGE,
|
||||
tags = bindingTags(channelId, epoch) + extraTags,
|
||||
content = text,
|
||||
authorPubKey,
|
||||
ChatEvent.build(text, createdAt) {
|
||||
channelBinding(channelId, epoch)
|
||||
extraTags.forEach { addUnique(it) }
|
||||
},
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -91,10 +86,12 @@ object ChannelChat {
|
||||
)
|
||||
|
||||
/**
|
||||
* Builds an unsigned kind-7 reaction rumor bound to [channelId]/[epoch] against
|
||||
* the target message ([targetId]/[targetAuthor]/[targetKind]). [content] is the
|
||||
* reaction (e.g. `"+"`, `"🤙"`). On the receiving side this decrypts to a normal
|
||||
* kind-7 that wires to its target Note by the `e` tag through the shared cache.
|
||||
* Builds an unsigned kind-7 [ReactionEvent] rumor bound to [channelId]/[epoch]
|
||||
* against the target message ([targetId]/[targetAuthor]/[targetKind]). [content]
|
||||
* is the reaction (e.g. `"+"`, `"🤙"`). Kept to the minimal `e`/`p`/`k` tag form
|
||||
* (no relay hints) so it stays wire-identical across clients. On the receiving
|
||||
* side it decrypts to a normal kind-7 that wires to its target Note by the `e`
|
||||
* tag through the shared cache.
|
||||
*/
|
||||
fun reaction(
|
||||
authorPubKey: HexKey,
|
||||
@@ -106,34 +103,34 @@ object ChannelChat {
|
||||
content: String,
|
||||
createdAt: Long,
|
||||
): Event =
|
||||
RumorAssembler.assembleRumor(
|
||||
RumorAssembler.assembleRumor<ReactionEvent>(
|
||||
pubKey = authorPubKey,
|
||||
createdAt = createdAt,
|
||||
kind = ConcordKinds.REACTION,
|
||||
kind = ReactionEvent.KIND,
|
||||
tags =
|
||||
bindingTags(channelId, epoch) +
|
||||
arrayOf(
|
||||
arrayOf("e", targetId),
|
||||
arrayOf("p", targetAuthor),
|
||||
arrayOf("k", targetKind.toString()),
|
||||
),
|
||||
arrayOf(
|
||||
ChannelTag.assemble(channelId),
|
||||
EpochTag.assemble(epoch),
|
||||
arrayOf("e", targetId),
|
||||
arrayOf("p", targetAuthor),
|
||||
arrayOf("k", targetKind.toString()),
|
||||
),
|
||||
content = content,
|
||||
)
|
||||
|
||||
/** The channel id a Chat Plane [rumor] is bound to, or null if unbound. */
|
||||
fun channelOf(rumor: Event): HexKey? = rumor.tags.firstTagValue(TAG_CHANNEL)
|
||||
fun channelOf(rumor: Event): HexKey? = rumor.tags.concordChannel()
|
||||
|
||||
/** The epoch a Chat Plane [rumor] is bound to, or null if unbound/malformed. */
|
||||
fun epochOf(rumor: Event): Long? = rumor.tags.firstTagValue(TAG_EPOCH)?.toLongOrNull()
|
||||
fun epochOf(rumor: Event): Long? = rumor.tags.concordEpoch()
|
||||
|
||||
/**
|
||||
* True when [rumor] is bound to exactly [channelId] and [epoch]. Recipients
|
||||
* must reject any Chat Plane event whose binding does not match the plane it
|
||||
* arrived on.
|
||||
* True when [rumor] is bound to exactly [channelId] and [epoch]. Recipients must
|
||||
* reject any Chat Plane event whose binding does not match the plane it arrived on.
|
||||
*/
|
||||
fun isBoundTo(
|
||||
rumor: Event,
|
||||
channelId: HexKey,
|
||||
epoch: Long,
|
||||
): Boolean = channelOf(rumor) == channelId && epochOf(rumor) == epoch
|
||||
): Boolean = rumor.tags.isConcordBoundTo(channelId, epoch)
|
||||
}
|
||||
|
||||
+46
@@ -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.concord.cord03Channels
|
||||
|
||||
import com.vitorpamplona.quartz.concord.cord03Channels.tags.ChannelTag
|
||||
import com.vitorpamplona.quartz.concord.cord03Channels.tags.EpochTag
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
||||
|
||||
/**
|
||||
* The Concord Chat Plane binding, added to any standard event (kind 9 chat, 7
|
||||
* reaction, 5 delete, …) that is published on a channel plane. Because the binding
|
||||
* layers onto reused standard events, these extensions are generic over the event
|
||||
* type instead of pinned to a Concord-specific one.
|
||||
*/
|
||||
fun <T : Event> TagArrayBuilder<T>.channel(channelId: HexKey) = addUnique(ChannelTag.assemble(channelId))
|
||||
|
||||
fun <T : Event> TagArrayBuilder<T>.epoch(epoch: Long) = addUnique(EpochTag.assemble(epoch))
|
||||
|
||||
/** Binds an event to [channelId] at [epoch] — both tags every Chat Plane rumor carries. */
|
||||
fun <T : Event> TagArrayBuilder<T>.channelBinding(
|
||||
channelId: HexKey,
|
||||
epoch: Long,
|
||||
) = apply {
|
||||
channel(channelId)
|
||||
epoch(epoch)
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* 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.concord.cord03Channels
|
||||
|
||||
import com.vitorpamplona.quartz.concord.cord03Channels.tags.ChannelTag
|
||||
import com.vitorpamplona.quartz.concord.cord03Channels.tags.EpochTag
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArray
|
||||
|
||||
/** The channel id this Chat Plane rumor is bound to, or null if unbound. */
|
||||
fun TagArray.concordChannel(): HexKey? = firstNotNullOfOrNull(ChannelTag::parse)
|
||||
|
||||
/** The epoch this Chat Plane rumor is bound to, or null if unbound/malformed. */
|
||||
fun TagArray.concordEpoch(): Long? = firstNotNullOfOrNull(EpochTag::parse)
|
||||
|
||||
/**
|
||||
* True when these tags bind to exactly [channelId] and [epoch]. Recipients must
|
||||
* reject any Chat Plane event whose binding does not match the plane it arrived on.
|
||||
*/
|
||||
fun TagArray.isConcordBoundTo(
|
||||
channelId: HexKey,
|
||||
epoch: Long,
|
||||
): Boolean = concordChannel() == channelId && concordEpoch() == epoch
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* 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.concord.cord03Channels.tags
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.has
|
||||
import com.vitorpamplona.quartz.utils.ensure
|
||||
|
||||
/**
|
||||
* The `["channel", <id>]` tag that binds a Concord Chat Plane rumor to the channel
|
||||
* it belongs to (CORD-03). Present on every message/reply/reaction/edit/delete so a
|
||||
* recipient can reject an event lifted from another channel.
|
||||
*/
|
||||
class ChannelTag {
|
||||
companion object {
|
||||
const val TAG_NAME = "channel"
|
||||
|
||||
fun isTag(tag: Array<String>) = tag.has(1) && tag[0] == TAG_NAME && tag[1].isNotEmpty()
|
||||
|
||||
fun parse(tag: Array<String>): HexKey? {
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].isNotEmpty()) { return null }
|
||||
return tag[1]
|
||||
}
|
||||
|
||||
fun assemble(channelId: HexKey) = arrayOf(TAG_NAME, channelId)
|
||||
}
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* 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.concord.cord03Channels.tags
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.has
|
||||
import com.vitorpamplona.quartz.utils.ensure
|
||||
|
||||
/**
|
||||
* The `["epoch", <n>]` tag that binds a Concord Chat Plane rumor to the community
|
||||
* epoch it was authored under (CORD-03). Paired with [ChannelTag]; an event whose
|
||||
* epoch does not match the plane it arrived on is rejected, so a message can't be
|
||||
* replayed across a rekey.
|
||||
*/
|
||||
class EpochTag {
|
||||
companion object {
|
||||
const val TAG_NAME = "epoch"
|
||||
|
||||
fun isTag(tag: Array<String>) = tag.has(1) && tag[0] == TAG_NAME && tag[1].isNotEmpty()
|
||||
|
||||
fun parse(tag: Array<String>): Long? {
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].isNotEmpty()) { return null }
|
||||
return tag[1].toLongOrNull()
|
||||
}
|
||||
|
||||
fun assemble(epoch: Long) = arrayOf(TAG_NAME, epoch.toString())
|
||||
}
|
||||
}
|
||||
+5
-2
@@ -21,6 +21,8 @@
|
||||
package com.vitorpamplona.quartz.concord.cord07Voice
|
||||
|
||||
import com.vitorpamplona.quartz.concord.cord03Channels.ChannelChat
|
||||
import com.vitorpamplona.quartz.concord.cord03Channels.tags.ChannelTag
|
||||
import com.vitorpamplona.quartz.concord.cord03Channels.tags.EpochTag
|
||||
import com.vitorpamplona.quartz.concord.events.ConcordKinds
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
@@ -69,7 +71,8 @@ object VoicePresence {
|
||||
subMs: Int? = null,
|
||||
): Event {
|
||||
val tags = ArrayList<Array<String>>()
|
||||
tags.addAll(ChannelChat.bindingTags(channelId, epoch))
|
||||
tags.add(ChannelTag.assemble(channelId))
|
||||
tags.add(EpochTag.assemble(epoch))
|
||||
tags.add(arrayOf(TAG_IDENTITY, identity))
|
||||
if (broker != null) tags.add(arrayOf(TAG_BROKER, broker))
|
||||
if (subMs != null) tags.add(arrayOf("ms", subMs.toString()))
|
||||
@@ -82,7 +85,7 @@ object VoicePresence {
|
||||
channelId: HexKey,
|
||||
epoch: Long,
|
||||
createdAt: Long,
|
||||
): Event = RumorAssembler.assembleRumor(authorPubKey, createdAt, KIND, ChannelChat.bindingTags(channelId, epoch), CONTENT_LEFT)
|
||||
): Event = RumorAssembler.assembleRumor(authorPubKey, createdAt, KIND, arrayOf(ChannelTag.assemble(channelId), EpochTag.assemble(epoch)), CONTENT_LEFT)
|
||||
|
||||
fun parse(rumor: Event): VoicePresenceInfo? {
|
||||
if (rumor.kind != KIND) return null
|
||||
|
||||
+5
-5
@@ -37,11 +37,11 @@ object ConcordKinds {
|
||||
const val SEAL_ENCRYPTED = 20013
|
||||
const val SEAL_PLAINTEXT = 20014
|
||||
|
||||
// Chat Plane rumors (CORD-03)
|
||||
const val MESSAGE = 9
|
||||
const val COMMENT = 1111
|
||||
const val REACTION = 7
|
||||
const val DELETE = 5
|
||||
// Chat Plane rumors (CORD-03).
|
||||
// Messages (kind 9), replies (9 + q), reactions (7), and deletes (5) are standard
|
||||
// Nostr events — Concord reuses ChatEvent / ReactionEvent / DeletionEvent and only
|
||||
// adds the channel/epoch binding (see cord03Channels/ChannelChat + tags/), so they
|
||||
// are NOT aliased here. Only the Concord-specific chat kinds remain.
|
||||
const val EDIT = 3302
|
||||
const val WEBXDC = 3310
|
||||
const val TYPING = 23311
|
||||
|
||||
Reference in New Issue
Block a user