feat: EXT-007 P2P message history gossip
After hello verification, the connecting peer sends history_request to the first peer it meets (one per room, no fan-out). The responder queries SQLite and replies with a history_chunk. Received history is stored via INSERT OR IGNORE (mid dedup) and emitted as history_loaded IPC events. - proto: MsgHistoryRequest/Chunk types, HistoryEntry, EvtHistoryLoaded, ComputeMsgID (sha256 content-addressed ID), MsgID field on ChatMessage - store: ALTER TABLE ADD COLUMN msg_id + unique index migration (idempotent); RecentMessagesSince query (msg_id IS NOT NULL filter); msg_id persisted on save - mesh: RequestHistoryFrom, HandleHistoryRequest, HandleHistoryChunk methods; historyRequested/historyFirstPeer state to ensure single-peer requests - peer: dispatch history_request/history_chunk; RequestHistoryFrom after hello; stamp MsgID on incoming chat messages - ipc: stamp MsgID on outgoing group chat messages - EXTENSIONS.md: EXT-007 documented Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,7 +3,11 @@
|
||||
// Binary data (keys, signatures) is hex-encoded; signaling boxes are base64.
|
||||
package proto
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ── Identity ──────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -43,8 +47,10 @@ const (
|
||||
MsgFileAccept MsgType = "file-accept"
|
||||
MsgFileCancel MsgType = "file-cancel"
|
||||
MsgFileDone MsgType = "file-done"
|
||||
MsgPing MsgType = "ping"
|
||||
MsgPong MsgType = "pong"
|
||||
MsgPing MsgType = "ping"
|
||||
MsgPong MsgType = "pong"
|
||||
MsgHistoryRequest MsgType = "history_request"
|
||||
MsgHistoryChunk MsgType = "history_chunk"
|
||||
)
|
||||
|
||||
// PmMessage is a private message sent directly over a single peer link (§8 "pm").
|
||||
@@ -83,17 +89,43 @@ type PeerMessage struct {
|
||||
Reason string `json:"reason,omitempty"` // file-cancel
|
||||
|
||||
Seq *uint64 `json:"seq,omitempty"` // ping/pong
|
||||
|
||||
// history_request fields
|
||||
Since int64 `json:"since,omitempty"` // Unix ms; 0 = no lower bound
|
||||
Limit int `json:"limit,omitempty"`
|
||||
|
||||
// history_chunk fields
|
||||
History []HistoryEntry `json:"history,omitempty"`
|
||||
HistoryDone bool `json:"history_done,omitempty"`
|
||||
}
|
||||
|
||||
// HistoryEntry is one message in a history_chunk response.
|
||||
type HistoryEntry struct {
|
||||
Mid string `json:"mid"`
|
||||
From string `json:"from"` // peer ID hex
|
||||
FromAlias string `json:"from_alias"` // advisory
|
||||
Text string `json:"text"`
|
||||
Ts int64 `json:"ts"` // Unix ms
|
||||
}
|
||||
|
||||
// ChatMessage is a group chat message (wire type "chat", §8).
|
||||
// Also used internally for persisting PMs after they are received.
|
||||
type ChatMessage struct {
|
||||
Mid string `json:"mid,omitempty"` // optional dedup id (required when relay hops > 0)
|
||||
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
|
||||
Room string `json:"room"`
|
||||
Text string `json:"text"`
|
||||
Ts int64 `json:"ts"` // Unix milliseconds
|
||||
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
|
||||
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
|
||||
Room string `json:"room"`
|
||||
Text string `json:"text"`
|
||||
Ts int64 `json:"ts"` // Unix milliseconds
|
||||
}
|
||||
|
||||
// ComputeMsgID returns the EXT-007 content-addressed ID for a message.
|
||||
// sha256(fromID \x00 room \x00 ts_decimal \x00 text)
|
||||
func ComputeMsgID(fromID PeerID, room string, ts int64, text string) string {
|
||||
h := sha256.New()
|
||||
fmt.Fprintf(h, "%s\x00%s\x00%d\x00%s", string(fromID), room, ts, text)
|
||||
return fmt.Sprintf("sha256:%x", h.Sum(nil))
|
||||
}
|
||||
|
||||
// PeerGossip shares known peer addresses.
|
||||
@@ -259,6 +291,7 @@ const (
|
||||
EvtIdentityImported IpcMsgType = "identity_imported"
|
||||
EvtSharesList IpcMsgType = "shares_list"
|
||||
EvtRoomCreated IpcMsgType = "room_created" // field: room (name)
|
||||
EvtHistoryLoaded IpcMsgType = "history_loaded" // fields: room, messages
|
||||
)
|
||||
|
||||
// NetworkInfo summarises one joined network for state_snapshot and network_joined events.
|
||||
@@ -312,7 +345,8 @@ type IpcMessage struct {
|
||||
Networks []NetworkInfo `json:"networks,omitempty"`
|
||||
ErrorMessage string `json:"error_message,omitempty"`
|
||||
InviteGenerated string `json:"invite,omitempty"`
|
||||
Files []FileEntry `json:"files,omitempty"`
|
||||
Files []FileEntry `json:"files,omitempty"`
|
||||
Messages []ChatMessage `json:"messages,omitempty"` // history_loaded
|
||||
Shares []ShareEntry `json:"shares,omitempty"`
|
||||
ShareNetworks []string `json:"network_ids,omitempty"` // for add_share command: scope to specific network IDs, or ["*"] for global
|
||||
// export_identity / import_identity
|
||||
|
||||
Reference in New Issue
Block a user