Anchor: add EXT-009 scoped presence query
Lets a client ask "is peer X currently online in network Y?" without joining that network first — a stateless O(1) lookup against the anchor's existing client registry. Scoped to (net, id) rather than a global lookup by id alone, so it can't be used as a cross-network "is this pubkey online anywhere" oracle: the caller must already know a network the peer belongs to, matching the knowledge already required to join it and observe presence the slow way via peer-join/peer-leave. Motivated by flit's known-devices list, where each pairing is its own isolated anchor network and the app holds no persistent connection while idle — today the only way to know if a device is reachable is to attempt a full connect. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
106
EXTENSIONS.md
106
EXTENSIONS.md
@@ -379,3 +379,109 @@ clients receive the full reaction state on reconnect.
|
|||||||
- Incoming `reaction` wire frames are dispatched as `reaction` IPC events.
|
- Incoming `reaction` wire frames are dispatched as `reaction` IPC events.
|
||||||
- `BrowserAdapter.send()` handles `send_reaction` commands and both broadcasts to all peers and emits a local `reaction` event.
|
- `BrowserAdapter.send()` handles `send_reaction` commands and both broadcasts to all peers and emits a local `reaction` event.
|
||||||
- `sendChat` includes the `mid` in the wire frame so reactions can reference it correctly across peers.
|
- `sendChat` includes the `mid` in the wire frame so reactions can reference it correctly across peers.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## EXT-009 — Scoped Presence Query
|
||||||
|
|
||||||
|
**Status:** proposed
|
||||||
|
**Affects:** anchor WS protocol only (new request/response pair); no DataChannel or peer-to-peer wire changes
|
||||||
|
|
||||||
|
### Motivation
|
||||||
|
|
||||||
|
YAW/2 §5.2 presence (`peer-join` / `peer-leave`) is push-only and scoped to peers
|
||||||
|
who are simultaneously joined to the same network — you only learn someone is
|
||||||
|
online by already being in the room with them. That's fine for chat, but it's
|
||||||
|
the wrong shape for flit's "known devices" list: each paired device is its own
|
||||||
|
isolated network (`net = NetHash(PairRoomName(idA, idB))`), and the app doesn't
|
||||||
|
hold a persistent anchor connection per pairing while idle. Today the only way
|
||||||
|
to find out if a known device is reachable is to attempt a full connect.
|
||||||
|
|
||||||
|
This extension adds a lightweight, stateless query: "is peer X currently
|
||||||
|
online in network Y?" — answerable from the anchor's existing in-memory
|
||||||
|
`clients` registry with an O(1) lookup, no new server-side state.
|
||||||
|
|
||||||
|
**Deliberately not** a global "is this pubkey online anywhere" query. The
|
||||||
|
anchor's `clients` map is keyed globally by peer id, so an unscoped query
|
||||||
|
would let anyone who knows a pubkey probe its online status across every
|
||||||
|
network on the anchor, with no relationship required — a cross-identity
|
||||||
|
presence oracle. Scoping the query to `(net, id)` keeps the access model
|
||||||
|
identical to today's: knowing a network's hash is already the bar for
|
||||||
|
joining it and observing presence the slow way (§5.1 `joined`, §5.2
|
||||||
|
`peer-join`); this extension only removes the need to actually join and wait.
|
||||||
|
|
||||||
|
### Wire messages
|
||||||
|
|
||||||
|
Sent over the anchor WS connection. Does **not** require a prior `join` —
|
||||||
|
the query is anchor-local and stateless, so a client may ask before joining,
|
||||||
|
after leaving, or without ever joining any network on this connection.
|
||||||
|
|
||||||
|
#### `presence_query`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "type": "presence_query", "net": "<64-hex net hash>", "id": "<64-hex peer id>" }
|
||||||
|
```
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|-------|--------|--------------|
|
||||||
|
| `net` | string | Network hash, computed the same way as `join` (§5.1). |
|
||||||
|
| `id` | string | Peer id being queried. |
|
||||||
|
|
||||||
|
#### `presence`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "type": "presence", "net": "<64-hex net hash>", "id": "<64-hex peer id>", "online": true }
|
||||||
|
```
|
||||||
|
|
||||||
|
`online` is `true` iff a client is currently registered with that exact
|
||||||
|
`(net, id)` pair. Unknown `net`/`id` combinations return `online: false`,
|
||||||
|
identical in shape to a peer who simply isn't connected — the anchor does
|
||||||
|
not distinguish "never seen this net" from "peer not currently in it."
|
||||||
|
|
||||||
|
### Anchor implementation notes
|
||||||
|
|
||||||
|
No new registry. `a.clients` is already keyed globally by peer id
|
||||||
|
(`cmd/anchor/main.go`); the query handler does:
|
||||||
|
|
||||||
|
```go
|
||||||
|
a.mu.RLock()
|
||||||
|
c, ok := a.clients[id]
|
||||||
|
online := ok && c.net == net
|
||||||
|
a.mu.RUnlock()
|
||||||
|
```
|
||||||
|
|
||||||
|
Same lock, same map, no writes. YAW/2-only anchors that don't implement this
|
||||||
|
extension simply never emit a `presence` reply — per §8, unknown request
|
||||||
|
types are ignored by clients, so callers should treat "no reply within a
|
||||||
|
short timeout" the same as `online: false` rather than blocking indefinitely.
|
||||||
|
|
||||||
|
### Security considerations
|
||||||
|
|
||||||
|
- **No new information beyond what §5.2 already permits** — the requester
|
||||||
|
must already know both `net` and `id` to ask, exactly the knowledge
|
||||||
|
required to join that network and observe presence natively. This
|
||||||
|
extension is a latency/statefulness shortcut, not a new capability.
|
||||||
|
- **Does not enable identity-wide presence enumeration.** Because the query
|
||||||
|
is scoped to a specific `net`, checking whether pubkey X is online
|
||||||
|
requires already knowing at least one network X is a member of. An anchor
|
||||||
|
operator or a client cannot use this to ask "where is X online" across
|
||||||
|
all networks it serves.
|
||||||
|
- **Rate limiting recommended** at the anchor (e.g. per-connection token
|
||||||
|
bucket) to prevent a connection from being used to hammer many `(net,
|
||||||
|
id)` guesses cheaply. This mirrors the existing `history_request`
|
||||||
|
rate-limit precedent (EXT-007, one request per peer/room per 60s) even
|
||||||
|
though the abuse shape here is different — presence queries are free of
|
||||||
|
disk I/O but still worth bounding.
|
||||||
|
- **No change to what the anchor can already infer.** An anchor could
|
||||||
|
already tell that two ids are members of the same net by virtue of
|
||||||
|
relaying between them; this extension doesn't let the anchor learn new
|
||||||
|
relationships, only lets *clients* ask a question the anchor already had
|
||||||
|
the answer to.
|
||||||
|
|
||||||
|
### Client usage (flit)
|
||||||
|
|
||||||
|
Each known device already has a deterministic `net` (`PairRoomName(idA,
|
||||||
|
idB)`, hashed). A "known devices" list can send one `presence_query` per
|
||||||
|
entry — no persistent connection required per pairing — and render
|
||||||
|
online/offline before the user taps Connect. A short client-side cache
|
||||||
|
(a few seconds) is recommended to avoid re-querying on every render.
|
||||||
|
|||||||
@@ -69,8 +69,30 @@ type client struct {
|
|||||||
net string // hashed network name, set after join
|
net string // hashed network name, set after join
|
||||||
send chan proto.AnchorMessage
|
send chan proto.AnchorMessage
|
||||||
conn *websocket.Conn
|
conn *websocket.Conn
|
||||||
|
|
||||||
|
// EXT-009 presence_query rate limiting. Only ever touched from this
|
||||||
|
// connection's own read-loop goroutine, so no lock needed.
|
||||||
|
presenceQueryCount int
|
||||||
|
presenceWindowStart time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// allowPresenceQuery implements a simple per-connection token bucket for
|
||||||
|
// EXT-009 presence_query: presenceQueryLimit requests per presenceQueryWindow.
|
||||||
|
func (c *client) allowPresenceQuery() bool {
|
||||||
|
now := time.Now()
|
||||||
|
if now.Sub(c.presenceWindowStart) > presenceQueryWindow {
|
||||||
|
c.presenceWindowStart = now
|
||||||
|
c.presenceQueryCount = 0
|
||||||
|
}
|
||||||
|
c.presenceQueryCount++
|
||||||
|
return c.presenceQueryCount <= presenceQueryLimit
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
presenceQueryLimit = 20
|
||||||
|
presenceQueryWindow = 10 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
type anchor struct {
|
type anchor struct {
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
clients map[string]*client // keyed by hex peer id
|
clients map[string]*client // keyed by hex peer id
|
||||||
@@ -109,6 +131,16 @@ func (a *anchor) unregister(c *client) {
|
|||||||
log.Printf("anchor: peer left: %s", c.id[:min(8, len(c.id))])
|
log.Printf("anchor: peer left: %s", c.id[:min(8, len(c.id))])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isOnline reports whether a client is currently registered with the exact
|
||||||
|
// (net, id) pair — EXT-009. O(1): reuses the existing global clients map,
|
||||||
|
// no new state.
|
||||||
|
func (a *anchor) isOnline(net, id string) bool {
|
||||||
|
a.mu.RLock()
|
||||||
|
defer a.mu.RUnlock()
|
||||||
|
c, ok := a.clients[id]
|
||||||
|
return ok && c.net == net
|
||||||
|
}
|
||||||
|
|
||||||
// networkPeerIDs returns the hex ids of all peers in the same network as netHash.
|
// networkPeerIDs returns the hex ids of all peers in the same network as netHash.
|
||||||
func (a *anchor) networkPeerIDs(netHash, excludeID string) []string {
|
func (a *anchor) networkPeerIDs(netHash, excludeID string) []string {
|
||||||
a.mu.RLock()
|
a.mu.RLock()
|
||||||
@@ -225,6 +257,24 @@ func (a *anchor) handleWS(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
log.Printf("anchor: peer joined: %s net=%s peers=%d", c.id[:min(8, len(c.id))], msg.Net[:8], len(peers))
|
log.Printf("anchor: peer joined: %s net=%s peers=%d", c.id[:min(8, len(c.id))], msg.Net[:8], len(peers))
|
||||||
|
|
||||||
|
case proto.AnchorPresenceQuery:
|
||||||
|
// EXT-009. Stateless and anchor-local: does not require the
|
||||||
|
// querying connection to have joined any network. Scoped to
|
||||||
|
// (net, id) — never a global "is this pubkey online anywhere"
|
||||||
|
// lookup, to avoid a cross-network presence oracle.
|
||||||
|
if msg.Net == "" || msg.ID == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !c.allowPresenceQuery() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
online := a.isOnline(msg.Net, msg.ID)
|
||||||
|
resp := proto.AnchorMessage{Type: proto.AnchorPresence, Net: msg.Net, ID: msg.ID, Online: &online}
|
||||||
|
select {
|
||||||
|
case c.send <- resp:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
case proto.AnchorTo:
|
case proto.AnchorTo:
|
||||||
if c.id == "" {
|
if c.id == "" {
|
||||||
continue // not joined yet
|
continue // not joined yet
|
||||||
|
|||||||
@@ -38,20 +38,20 @@ type PeerInfo struct {
|
|||||||
type MsgType string
|
type MsgType string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
MsgChat MsgType = "chat"
|
MsgChat MsgType = "chat"
|
||||||
MsgPm MsgType = "pm" // private message, §8
|
MsgPm MsgType = "pm" // private message, §8
|
||||||
MsgPeerGossip MsgType = "peer_gossip"
|
MsgPeerGossip MsgType = "peer_gossip"
|
||||||
MsgFileListReq MsgType = "file_list_req"
|
MsgFileListReq MsgType = "file_list_req"
|
||||||
MsgFileListResp MsgType = "file_list_resp"
|
MsgFileListResp MsgType = "file_list_resp"
|
||||||
MsgFileOffer MsgType = "file-offer" // §9, hyphenated per spec
|
MsgFileOffer MsgType = "file-offer" // §9, hyphenated per spec
|
||||||
MsgFileAccept MsgType = "file-accept"
|
MsgFileAccept MsgType = "file-accept"
|
||||||
MsgFileCancel MsgType = "file-cancel"
|
MsgFileCancel MsgType = "file-cancel"
|
||||||
MsgFileDone MsgType = "file-done"
|
MsgFileDone MsgType = "file-done"
|
||||||
MsgPing MsgType = "ping"
|
MsgPing MsgType = "ping"
|
||||||
MsgPong MsgType = "pong"
|
MsgPong MsgType = "pong"
|
||||||
MsgHistoryRequest MsgType = "history_request"
|
MsgHistoryRequest MsgType = "history_request"
|
||||||
MsgHistoryChunk MsgType = "history_chunk"
|
MsgHistoryChunk MsgType = "history_chunk"
|
||||||
MsgReaction MsgType = "reaction"
|
MsgReaction MsgType = "reaction"
|
||||||
)
|
)
|
||||||
|
|
||||||
// PmMessage is a private message sent directly over a single peer link (§8 "pm").
|
// PmMessage is a private message sent directly over a single peer link (§8 "pm").
|
||||||
@@ -108,9 +108,9 @@ type PeerMessage struct {
|
|||||||
type ResumableFile struct {
|
type ResumableFile struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
SHA256 string `json:"sha256"`
|
SHA256 string `json:"sha256"`
|
||||||
From string `json:"from"` // peer ID hex
|
From string `json:"from"` // peer ID hex
|
||||||
Size int64 `json:"size"`
|
Size int64 `json:"size"`
|
||||||
Offset int64 `json:"offset"` // bytes already received
|
Offset int64 `json:"offset"` // bytes already received
|
||||||
}
|
}
|
||||||
|
|
||||||
// HistoryEntry is one message in a history_chunk response.
|
// HistoryEntry is one message in a history_chunk response.
|
||||||
@@ -125,10 +125,10 @@ type HistoryEntry struct {
|
|||||||
// ChatMessage is a group chat message (wire type "chat", §8).
|
// ChatMessage is a group chat message (wire type "chat", §8).
|
||||||
// Also used internally for persisting PMs after they are received.
|
// Also used internally for persisting PMs after they are received.
|
||||||
type ChatMessage struct {
|
type ChatMessage struct {
|
||||||
Mid string `json:"mid,omitempty"` // optional dedup id (required when relay hops > 0)
|
Mid string `json:"mid,omitempty"` // optional dedup id (required when relay hops > 0)
|
||||||
MsgID string `json:"msg_id,omitempty"` // EXT-007: content-addressed gossip ID
|
MsgID string `json:"msg_id,omitempty"` // EXT-007: content-addressed gossip ID
|
||||||
From PeerID `json:"from,omitempty"` // set by receiver from DC context; not on wire for pm
|
From PeerID `json:"from,omitempty"` // set by receiver from DC context; not on wire for pm
|
||||||
To *PeerID `json:"to,omitempty"` // internal only — not transmitted; set for DMs
|
To *PeerID `json:"to,omitempty"` // internal only — not transmitted; set for DMs
|
||||||
Room string `json:"room"`
|
Room string `json:"room"`
|
||||||
Text string `json:"text"`
|
Text string `json:"text"`
|
||||||
Ts int64 `json:"ts"` // Unix milliseconds
|
Ts int64 `json:"ts"` // Unix milliseconds
|
||||||
@@ -240,27 +240,30 @@ type SignalingPayload struct {
|
|||||||
type AnchorMsgType string
|
type AnchorMsgType string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
AnchorChallenge AnchorMsgType = "challenge"
|
AnchorChallenge AnchorMsgType = "challenge"
|
||||||
AnchorJoin AnchorMsgType = "join"
|
AnchorJoin AnchorMsgType = "join"
|
||||||
AnchorJoined AnchorMsgType = "joined"
|
AnchorJoined AnchorMsgType = "joined"
|
||||||
AnchorPeerJoin AnchorMsgType = "peer-join"
|
AnchorPeerJoin AnchorMsgType = "peer-join"
|
||||||
AnchorPeerLeave AnchorMsgType = "peer-leave"
|
AnchorPeerLeave AnchorMsgType = "peer-leave"
|
||||||
AnchorTo AnchorMsgType = "to"
|
AnchorTo AnchorMsgType = "to"
|
||||||
AnchorFrom AnchorMsgType = "from"
|
AnchorFrom AnchorMsgType = "from"
|
||||||
AnchorNoPeer AnchorMsgType = "no-peer"
|
AnchorNoPeer AnchorMsgType = "no-peer"
|
||||||
|
AnchorPresenceQuery AnchorMsgType = "presence_query" // waste-go ext EXT-009
|
||||||
|
AnchorPresence AnchorMsgType = "presence" // waste-go ext EXT-009
|
||||||
)
|
)
|
||||||
|
|
||||||
// AnchorMessage covers all WebSocket frames to/from the anchor.
|
// AnchorMessage covers all WebSocket frames to/from the anchor.
|
||||||
type AnchorMessage struct {
|
type AnchorMessage struct {
|
||||||
Type AnchorMsgType `json:"type"`
|
Type AnchorMsgType `json:"type"`
|
||||||
Nonce string `json:"nonce,omitempty"` // challenge nonce, hex
|
Nonce string `json:"nonce,omitempty"` // challenge nonce, hex
|
||||||
ID string `json:"id,omitempty"` // peer hex id
|
ID string `json:"id,omitempty"` // peer hex id
|
||||||
Net string `json:"net,omitempty"` // hashed network name
|
Net string `json:"net,omitempty"` // hashed network name
|
||||||
Sig string `json:"sig,omitempty"` // ed25519 sig over (nonce||net), hex
|
Sig string `json:"sig,omitempty"` // ed25519 sig over (nonce||net), hex
|
||||||
Peers []string `json:"peers,omitempty"` // joined: list of peer hex ids in network
|
Peers []string `json:"peers,omitempty"` // joined: list of peer hex ids in network
|
||||||
To string `json:"to,omitempty"` // target peer hex id
|
To string `json:"to,omitempty"` // target peer hex id
|
||||||
From string `json:"from,omitempty"` // sender peer hex id
|
From string `json:"from,omitempty"` // sender peer hex id
|
||||||
Box string `json:"box,omitempty"` // base64 nacl/box sealed payload
|
Box string `json:"box,omitempty"` // base64 nacl/box sealed payload
|
||||||
|
Online *bool `json:"online,omitempty"` // presence: true iff (net, id) is currently connected
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── IPC protocol (daemon ↔ local UI) ─────────────────────────────────────────
|
// ── IPC protocol (daemon ↔ local UI) ─────────────────────────────────────────
|
||||||
@@ -270,45 +273,45 @@ type IpcMsgType string
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
// Commands (UI → daemon)
|
// Commands (UI → daemon)
|
||||||
CmdSendMessage IpcMsgType = "send_message"
|
CmdSendMessage IpcMsgType = "send_message"
|
||||||
CmdJoinNetwork IpcMsgType = "join_network" // fields: network_name (plaintext)
|
CmdJoinNetwork IpcMsgType = "join_network" // fields: network_name (plaintext)
|
||||||
CmdLeaveNetwork IpcMsgType = "leave_network"
|
CmdLeaveNetwork IpcMsgType = "leave_network"
|
||||||
CmdGetState IpcMsgType = "get_state"
|
CmdGetState IpcMsgType = "get_state"
|
||||||
CmdSendFile IpcMsgType = "send_file"
|
CmdSendFile IpcMsgType = "send_file"
|
||||||
CmdSetShareDir IpcMsgType = "set_share_dir" // set per-network share directory at runtime
|
CmdSetShareDir IpcMsgType = "set_share_dir" // set per-network share directory at runtime
|
||||||
CmdGenerateInvite IpcMsgType = "generate_invite"
|
CmdGenerateInvite IpcMsgType = "generate_invite"
|
||||||
CmdGetFileList IpcMsgType = "get_file_list"
|
CmdGetFileList IpcMsgType = "get_file_list"
|
||||||
CmdExportIdentity IpcMsgType = "export_identity" // returns encrypted backup blob
|
CmdExportIdentity IpcMsgType = "export_identity" // returns encrypted backup blob
|
||||||
CmdImportIdentity IpcMsgType = "import_identity" // replaces identity from backup blob
|
CmdImportIdentity IpcMsgType = "import_identity" // replaces identity from backup blob
|
||||||
CmdAddShare IpcMsgType = "add_share" // add a share root; fields: path, networks
|
CmdAddShare IpcMsgType = "add_share" // add a share root; fields: path, networks
|
||||||
CmdRemoveShare IpcMsgType = "remove_share" // remove a share root; field: path
|
CmdRemoveShare IpcMsgType = "remove_share" // remove a share root; field: path
|
||||||
CmdListShares IpcMsgType = "list_shares" // returns shares_list event
|
CmdListShares IpcMsgType = "list_shares" // returns shares_list event
|
||||||
CmdCreateRoom IpcMsgType = "create_room" // field: room (name)
|
CmdCreateRoom IpcMsgType = "create_room" // field: room (name)
|
||||||
CmdSetDownloadDir IpcMsgType = "set_download_dir" // set per-network download directory at runtime; fields: network_id, path
|
CmdSetDownloadDir IpcMsgType = "set_download_dir" // set per-network download directory at runtime; fields: network_id, path
|
||||||
CmdSendReaction IpcMsgType = "send_reaction" // fields: network_id, reaction_mid, reaction_emoji
|
CmdSendReaction IpcMsgType = "send_reaction" // fields: network_id, reaction_mid, reaction_emoji
|
||||||
|
|
||||||
// Events (daemon → UI)
|
// Events (daemon → UI)
|
||||||
EvtMessageReceived IpcMsgType = "message_received"
|
EvtMessageReceived IpcMsgType = "message_received"
|
||||||
EvtPeerConnected IpcMsgType = "peer_connected"
|
EvtPeerConnected IpcMsgType = "peer_connected"
|
||||||
EvtPeerDisconnected IpcMsgType = "peer_disconnected"
|
EvtPeerDisconnected IpcMsgType = "peer_disconnected"
|
||||||
EvtSessionReady IpcMsgType = "session_ready" // DataChannel open + hello verified
|
EvtSessionReady IpcMsgType = "session_ready" // DataChannel open + hello verified
|
||||||
EvtPeerStatus IpcMsgType = "peer_status" // ICE connection state + candidate type
|
EvtPeerStatus IpcMsgType = "peer_status" // ICE connection state + candidate type
|
||||||
EvtIncomingFile IpcMsgType = "incoming_file"
|
EvtIncomingFile IpcMsgType = "incoming_file"
|
||||||
EvtFileProgress IpcMsgType = "file_progress"
|
EvtFileProgress IpcMsgType = "file_progress"
|
||||||
EvtStateSnapshot IpcMsgType = "state_snapshot"
|
EvtStateSnapshot IpcMsgType = "state_snapshot"
|
||||||
EvtError IpcMsgType = "error"
|
EvtError IpcMsgType = "error"
|
||||||
EvtInviteGenerated IpcMsgType = "invite_generated"
|
EvtInviteGenerated IpcMsgType = "invite_generated"
|
||||||
EvtFileList IpcMsgType = "file_list"
|
EvtFileList IpcMsgType = "file_list"
|
||||||
EvtFileComplete IpcMsgType = "file_complete"
|
EvtFileComplete IpcMsgType = "file_complete"
|
||||||
EvtNetworkJoined IpcMsgType = "network_joined"
|
EvtNetworkJoined IpcMsgType = "network_joined"
|
||||||
EvtNetworkLeft IpcMsgType = "network_left"
|
EvtNetworkLeft IpcMsgType = "network_left"
|
||||||
EvtIdentityExported IpcMsgType = "identity_exported"
|
EvtIdentityExported IpcMsgType = "identity_exported"
|
||||||
EvtIdentityImported IpcMsgType = "identity_imported"
|
EvtIdentityImported IpcMsgType = "identity_imported"
|
||||||
EvtSharesList IpcMsgType = "shares_list"
|
EvtSharesList IpcMsgType = "shares_list"
|
||||||
EvtRoomCreated IpcMsgType = "room_created" // field: room (name)
|
EvtRoomCreated IpcMsgType = "room_created" // field: room (name)
|
||||||
EvtHistoryLoaded IpcMsgType = "history_loaded" // fields: room, messages
|
EvtHistoryLoaded IpcMsgType = "history_loaded" // fields: room, messages
|
||||||
EvtResumableTransfers IpcMsgType = "resumable_transfers" // field: resumable_files
|
EvtResumableTransfers IpcMsgType = "resumable_transfers" // field: resumable_files
|
||||||
EvtReaction IpcMsgType = "reaction" // fields: reaction_mid, reaction_emoji, peer_id
|
EvtReaction IpcMsgType = "reaction" // fields: reaction_mid, reaction_emoji, peer_id
|
||||||
)
|
)
|
||||||
|
|
||||||
// NetworkInfo summarises one joined network for state_snapshot and network_joined events.
|
// NetworkInfo summarises one joined network for state_snapshot and network_joined events.
|
||||||
@@ -335,44 +338,44 @@ type IpcMessage struct {
|
|||||||
|
|
||||||
// join_network / leave_network
|
// join_network / leave_network
|
||||||
NetworkName string `json:"network_name,omitempty"`
|
NetworkName string `json:"network_name,omitempty"`
|
||||||
NetworkHash string `json:"network_hash,omitempty"` // 64-char hex (yaw2 `net` field); alternative to network_name
|
NetworkHash string `json:"network_hash,omitempty"` // 64-char hex (yaw2 `net` field); alternative to network_name
|
||||||
ShareDir string `json:"share_dir,omitempty"` // optional per-network share directory
|
ShareDir string `json:"share_dir,omitempty"` // optional per-network share directory
|
||||||
RequireInvite bool `json:"require_invite,omitempty"` // waste-go ext: reject peers without valid signed invite
|
RequireInvite bool `json:"require_invite,omitempty"` // waste-go ext: reject peers without valid signed invite
|
||||||
InviteString string `json:"invite_string,omitempty"` // waste-go ext: the invite used to join (stored in mesh)
|
InviteString string `json:"invite_string,omitempty"` // waste-go ext: the invite used to join (stored in mesh)
|
||||||
|
|
||||||
// send_file / set_share_dir / file_complete path
|
// send_file / set_share_dir / file_complete path
|
||||||
Path string `json:"path,omitempty"`
|
Path string `json:"path,omitempty"`
|
||||||
|
|
||||||
// events
|
// events
|
||||||
Peer *PeerInfo `json:"peer,omitempty"`
|
Peer *PeerInfo `json:"peer,omitempty"`
|
||||||
PeerID *PeerID `json:"peer_id,omitempty"`
|
PeerID *PeerID `json:"peer_id,omitempty"`
|
||||||
Nick string `json:"nick,omitempty"`
|
Nick string `json:"nick,omitempty"`
|
||||||
Message *ChatMessage `json:"message,omitempty"`
|
Message *ChatMessage `json:"message,omitempty"`
|
||||||
Offer *FileOffer `json:"offer,omitempty"`
|
Offer *FileOffer `json:"offer,omitempty"`
|
||||||
TransferID string `json:"transfer_id,omitempty"`
|
TransferID string `json:"transfer_id,omitempty"`
|
||||||
BytesReceived int64 `json:"bytes_received,omitempty"`
|
BytesReceived int64 `json:"bytes_received,omitempty"`
|
||||||
TotalBytes int64 `json:"total_bytes,omitempty"`
|
TotalBytes int64 `json:"total_bytes,omitempty"`
|
||||||
// state_snapshot fields (existing shape preserved for backward compat)
|
// state_snapshot fields (existing shape preserved for backward compat)
|
||||||
MasterAlias string `json:"master_alias,omitempty"` // daemon's alias (available before any network join)
|
MasterAlias string `json:"master_alias,omitempty"` // daemon's alias (available before any network join)
|
||||||
MasterID string `json:"master_id,omitempty"` // daemon's master public key hex
|
MasterID string `json:"master_id,omitempty"` // daemon's master public key hex
|
||||||
LocalPeer *PeerInfo `json:"local_peer,omitempty"`
|
LocalPeer *PeerInfo `json:"local_peer,omitempty"`
|
||||||
ConnectedPeers []PeerInfo `json:"connected_peers,omitempty"`
|
ConnectedPeers []PeerInfo `json:"connected_peers,omitempty"`
|
||||||
KnownPeers []PeerInfo `json:"known_peers,omitempty"` // historically seen, not currently connected
|
KnownPeers []PeerInfo `json:"known_peers,omitempty"` // historically seen, not currently connected
|
||||||
Rooms []string `json:"rooms,omitempty"`
|
Rooms []string `json:"rooms,omitempty"`
|
||||||
// multi-network: all joined networks (additive)
|
// multi-network: all joined networks (additive)
|
||||||
Networks []NetworkInfo `json:"networks,omitempty"`
|
Networks []NetworkInfo `json:"networks,omitempty"`
|
||||||
ErrorMessage string `json:"error_message,omitempty"`
|
ErrorMessage string `json:"error_message,omitempty"`
|
||||||
InviteGenerated string `json:"invite,omitempty"`
|
InviteGenerated string `json:"invite,omitempty"`
|
||||||
Files []FileEntry `json:"files,omitempty"`
|
Files []FileEntry `json:"files,omitempty"`
|
||||||
Messages []ChatMessage `json:"messages,omitempty"` // history_loaded
|
Messages []ChatMessage `json:"messages,omitempty"` // history_loaded
|
||||||
ResumableFiles []ResumableFile `json:"resumable_files,omitempty"` // resumable_transfers
|
ResumableFiles []ResumableFile `json:"resumable_files,omitempty"` // resumable_transfers
|
||||||
ReactionMID string `json:"reaction_mid,omitempty"` // reaction
|
ReactionMID string `json:"reaction_mid,omitempty"` // reaction
|
||||||
ReactionEmoji string `json:"reaction_emoji,omitempty"` // reaction
|
ReactionEmoji string `json:"reaction_emoji,omitempty"` // reaction
|
||||||
Shares []ShareEntry `json:"shares,omitempty"`
|
Shares []ShareEntry `json:"shares,omitempty"`
|
||||||
ShareNetworks []string `json:"network_ids,omitempty"` // for add_share command: scope to specific network IDs, or ["*"] for global
|
ShareNetworks []string `json:"network_ids,omitempty"` // for add_share command: scope to specific network IDs, or ["*"] for global
|
||||||
// export_identity / import_identity
|
// export_identity / import_identity
|
||||||
Passphrase string `json:"passphrase,omitempty"` // import only; never echoed back
|
Passphrase string `json:"passphrase,omitempty"` // import only; never echoed back
|
||||||
Backup string `json:"backup,omitempty"` // JSON backup blob
|
Backup string `json:"backup,omitempty"` // JSON backup blob
|
||||||
// peer_status — ICE connection quality
|
// peer_status — ICE connection quality
|
||||||
ConnState string `json:"conn_state,omitempty"` // pion PeerConnectionState string
|
ConnState string `json:"conn_state,omitempty"` // pion PeerConnectionState string
|
||||||
CandidateType string `json:"candidate_type,omitempty"` // host | srflx | relay | unknown
|
CandidateType string `json:"candidate_type,omitempty"` // host | srflx | relay | unknown
|
||||||
|
|||||||
Reference in New Issue
Block a user