Add invite strings (waste: URI) for peer onboarding
- internal/invite: Encode/Decode waste:<base64json{anchor,network}>
- proto: CmdGenerateInvite + EvtInviteGenerated + InviteString field
- ipc: track active network name; handle generate_invite (returns invite
string when joined to a network, errors if not joined or no anchor)
- daemon: --join <invite> flag — decodes anchor URL + network name,
sets anchor and auto-joins on startup
- tui: --join <invite> flag — extracts network name, skips -network
requirement; ctrl+i generates invite and shows it full-screen;
Esc dismisses the invite overlay
- FUTURE.md: document multi-network derived-identity design
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
27
FUTURE.md
27
FUTURE.md
@@ -59,6 +59,33 @@ No DHT needed at small group scale (10–50 nodes). Keep it simple:
|
|||||||
- Public key = stable identity, not a mutable nickname
|
- Public key = stable identity, not a mutable nickname
|
||||||
- No phone number, no central registry — closer to Signal's model than WASTE's original unregistered aliases
|
- No phone number, no central registry — closer to Signal's model than WASTE's original unregistered aliases
|
||||||
|
|
||||||
|
### Multi-Network Support
|
||||||
|
|
||||||
|
A single client should be able to participate in multiple networks simultaneously (e.g. "work" and "friends") without leaking that both identities belong to the same person.
|
||||||
|
|
||||||
|
**Privacy constraint:** if the same Ed25519 keypair is used across networks, any peer who is a member of both networks can trivially correlate you. The anchor also sees the same public key across networks.
|
||||||
|
|
||||||
|
**Solution — per-network derived identities:**
|
||||||
|
- One master Ed25519 seed in `identity.json`
|
||||||
|
- Per-network keypair = `HKDF(masterSeed, "yaw2-net", networkHash)`
|
||||||
|
- Same master + same network name = same derived keypair (stable identity within a network)
|
||||||
|
- Different networks = different peer IDs; correlation is impossible without knowing both network names
|
||||||
|
- The anchor sees only the derived public key
|
||||||
|
|
||||||
|
**Daemon changes:**
|
||||||
|
- Replace the single `networkCancel` with a `map[networkID]*networkCtx`
|
||||||
|
- Each context holds its own: derived identity, mesh, anchor connection, store (`messages-<netHash>.db`)
|
||||||
|
- `join_network` returns a `network_id` token used to scope subsequent commands
|
||||||
|
|
||||||
|
**IPC changes (breaking):**
|
||||||
|
- All commands and events gain a `network_id` field
|
||||||
|
- `get_state` returns an array of all joined networks
|
||||||
|
- `join_network` responds with `network_joined` carrying the derived peer ID for that network
|
||||||
|
|
||||||
|
**TUI changes:**
|
||||||
|
- Top-level network switcher (e.g. `[work] [friends]`)
|
||||||
|
- Rooms and peers are scoped per network underneath
|
||||||
|
|
||||||
### Transport (Long-term)
|
### Transport (Long-term)
|
||||||
Current transport is TCP with custom framing. QUIC is worth revisiting once the core is solid — it gives multiplexing and better NAT traversal behavior essentially for free.
|
Current transport is TCP with custom framing. QUIC is worth revisiting once the core is solid — it gives multiplexing and better NAT traversal behavior essentially for free.
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
|
|
||||||
"github.com/waste-go/internal/anchor"
|
"github.com/waste-go/internal/anchor"
|
||||||
"github.com/waste-go/internal/crypto"
|
"github.com/waste-go/internal/crypto"
|
||||||
|
"github.com/waste-go/internal/invite"
|
||||||
"github.com/waste-go/internal/ipc"
|
"github.com/waste-go/internal/ipc"
|
||||||
"github.com/waste-go/internal/mesh"
|
"github.com/waste-go/internal/mesh"
|
||||||
"github.com/waste-go/internal/proto"
|
"github.com/waste-go/internal/proto"
|
||||||
@@ -22,8 +23,21 @@ func main() {
|
|||||||
alias := flag.String("alias", "anon", "display name shown to peers (advisory only)")
|
alias := flag.String("alias", "anon", "display name shown to peers (advisory only)")
|
||||||
ipcPort := flag.Int("ipc-port", 17337, "port for local IPC (UI connects here)")
|
ipcPort := flag.Int("ipc-port", 17337, "port for local IPC (UI connects here)")
|
||||||
anchorURL := flag.String("anchor", "", "anchor WebSocket URL, e.g. ws://your-vps:17339/ws")
|
anchorURL := flag.String("anchor", "", "anchor WebSocket URL, e.g. ws://your-vps:17339/ws")
|
||||||
|
joinInvite := flag.String("join", "", "waste: invite string — sets anchor URL and auto-joins the network on startup")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
|
// --join overrides/sets the anchor URL and triggers an auto-join.
|
||||||
|
var autoJoinNetwork string
|
||||||
|
if *joinInvite != "" {
|
||||||
|
inv, err := invite.Decode(*joinInvite)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("invalid invite: %v", err)
|
||||||
|
}
|
||||||
|
*anchorURL = inv.Anchor
|
||||||
|
autoJoinNetwork = inv.Network
|
||||||
|
log.Printf("daemon: invite decoded — anchor=%s network=%s", inv.Anchor, inv.Network)
|
||||||
|
}
|
||||||
|
|
||||||
dir := expandHome(*dataDir)
|
dir := expandHome(*dataDir)
|
||||||
id, err := crypto.LoadOrCreate(dir, *alias)
|
id, err := crypto.LoadOrCreate(dir, *alias)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -51,7 +65,14 @@ func main() {
|
|||||||
log.Printf("daemon: left network %q", networkName)
|
log.Printf("daemon: left network %q", networkName)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := ipc.Run(m, *ipcPort, joinFn); err != nil {
|
// Auto-join from --join flag before starting IPC (non-blocking).
|
||||||
|
if autoJoinNetwork != "" {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
_ = cancel // lifecycle managed by the joinFn / ipc.Run leave
|
||||||
|
go joinFn(ctx, autoJoinNetwork)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ipc.Run(m, *ipcPort, *anchorURL, joinFn); err != nil {
|
||||||
log.Fatalf("ipc: %v", err)
|
log.Fatalf("ipc: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import (
|
|||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
"github.com/charmbracelet/lipgloss"
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
|
||||||
|
"github.com/waste-go/internal/invite"
|
||||||
"github.com/waste-go/internal/proto"
|
"github.com/waste-go/internal/proto"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -116,6 +117,7 @@ type model struct {
|
|||||||
|
|
||||||
status string
|
status string
|
||||||
errMsg string
|
errMsg string
|
||||||
|
invitePopup string // non-empty = show invite overlay
|
||||||
}
|
}
|
||||||
|
|
||||||
func newModel(ipcPort int, network string) model {
|
func newModel(ipcPort int, network string) model {
|
||||||
@@ -200,7 +202,15 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
case msg.Type == tea.KeyCtrlC:
|
case msg.Type == tea.KeyCtrlC:
|
||||||
return m, tea.Quit
|
return m, tea.Quit
|
||||||
case msg.Type == tea.KeyEsc:
|
case msg.Type == tea.KeyEsc:
|
||||||
|
if m.invitePopup != "" {
|
||||||
|
m.invitePopup = ""
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
return m, tea.Quit
|
return m, tea.Quit
|
||||||
|
case msg.String() == "ctrl+i":
|
||||||
|
if m.enc != nil {
|
||||||
|
cmds = append(cmds, sendIPC(m.enc, proto.IpcMessage{Type: proto.CmdGenerateInvite}))
|
||||||
|
}
|
||||||
case msg.Type == tea.KeyEnter:
|
case msg.Type == tea.KeyEnter:
|
||||||
m, cmds = m.doSend(cmds)
|
m, cmds = m.doSend(cmds)
|
||||||
case msg.Type == tea.KeyTab:
|
case msg.Type == tea.KeyTab:
|
||||||
@@ -272,6 +282,9 @@ func (m model) applyEvent(evt proto.IpcMessage) model {
|
|||||||
m.peerOrder = filterIDs(m.peerOrder, pid)
|
m.peerOrder = filterIDs(m.peerOrder, pid)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case proto.EvtInviteGenerated:
|
||||||
|
m.invitePopup = evt.InviteString
|
||||||
|
|
||||||
case proto.EvtMessageReceived:
|
case proto.EvtMessageReceived:
|
||||||
if evt.Message != nil {
|
if evt.Message != nil {
|
||||||
msg := evt.Message
|
msg := evt.Message
|
||||||
@@ -417,11 +430,31 @@ func (m model) View() string {
|
|||||||
if m.errMsg != "" {
|
if m.errMsg != "" {
|
||||||
statusLine = styleErr.Render(" ✗ " + m.errMsg)
|
statusLine = styleErr.Render(" ✗ " + m.errMsg)
|
||||||
} else {
|
} else {
|
||||||
hint := " tab: rooms · pgup/dn: scroll · ctrl+c: quit"
|
hint := " tab: rooms · ctrl+i: invite · ctrl+c: quit"
|
||||||
statusLine = styleStatus.Width(m.width).Render(" " + m.status + hint)
|
statusLine = styleStatus.Width(m.width).Render(" " + m.status + hint)
|
||||||
}
|
}
|
||||||
|
|
||||||
return lipgloss.JoinVertical(lipgloss.Left, mainRow, inputBox, statusLine)
|
view := lipgloss.JoinVertical(lipgloss.Left, mainRow, inputBox, statusLine)
|
||||||
|
|
||||||
|
// ── invite popup (full-screen overlay) ───────────────────────────────────
|
||||||
|
if m.invitePopup != "" {
|
||||||
|
label := styleActive.Render("Invite — share this with anyone you want to add:")
|
||||||
|
code := lipgloss.NewStyle().
|
||||||
|
Border(lipgloss.RoundedBorder()).
|
||||||
|
BorderForeground(lipgloss.Color("238")).
|
||||||
|
Padding(0, 1).
|
||||||
|
Width(m.width - 6).
|
||||||
|
Render(m.invitePopup)
|
||||||
|
hint := styleStatus.Render("Esc to close · ctrl+c to quit")
|
||||||
|
popup := lipgloss.NewStyle().
|
||||||
|
Width(m.width).
|
||||||
|
Height(m.height).
|
||||||
|
Align(lipgloss.Center, lipgloss.Center).
|
||||||
|
Render(lipgloss.JoinVertical(lipgloss.Left, label, "", code, "", hint))
|
||||||
|
return popup
|
||||||
|
}
|
||||||
|
|
||||||
|
return view
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m model) renderRooms(boxH int) string {
|
func (m model) renderRooms(boxH int) string {
|
||||||
@@ -566,11 +599,22 @@ func min(a, b int) int {
|
|||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
ipcPort := flag.Int("ipc", 17337, "daemon IPC port")
|
ipcPort := flag.Int("ipc", 17337, "daemon IPC port")
|
||||||
network := flag.String("network", "", "network name to join on startup (required)")
|
network := flag.String("network", "", "network name to join on startup")
|
||||||
|
joinInvite := flag.String("join", "", "waste: invite string — auto-sets the network name")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
|
// --join overrides --network.
|
||||||
|
if *joinInvite != "" {
|
||||||
|
inv, err := invite.Decode(*joinInvite)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, "error: invalid invite:", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
*network = inv.Network
|
||||||
|
}
|
||||||
|
|
||||||
if *network == "" {
|
if *network == "" {
|
||||||
fmt.Fprintln(os.Stderr, "error: -network is required")
|
fmt.Fprintln(os.Stderr, "error: -network or -join is required")
|
||||||
flag.Usage()
|
flag.Usage()
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|||||||
54
internal/invite/invite.go
Normal file
54
internal/invite/invite.go
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
// Package invite encodes and decodes waste invite strings.
|
||||||
|
// An invite carries the anchor URL and network name needed to join a network.
|
||||||
|
// Format: "waste:<url-safe-base64(json)>"
|
||||||
|
package invite
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const prefix = "waste:"
|
||||||
|
|
||||||
|
// Invite holds the information needed to join a network.
|
||||||
|
type Invite struct {
|
||||||
|
Anchor string `json:"anchor"` // WebSocket anchor URL
|
||||||
|
Network string `json:"network"` // plaintext network name
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode returns a waste: invite string for the given anchor URL and network name.
|
||||||
|
func Encode(anchor, network string) (string, error) {
|
||||||
|
if anchor == "" {
|
||||||
|
return "", fmt.Errorf("anchor URL is required")
|
||||||
|
}
|
||||||
|
if network == "" {
|
||||||
|
return "", fmt.Errorf("network name is required")
|
||||||
|
}
|
||||||
|
b, err := json.Marshal(Invite{Anchor: anchor, Network: network})
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return prefix + base64.URLEncoding.EncodeToString(b), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode parses a waste: invite string and returns the Invite.
|
||||||
|
func Decode(s string) (Invite, error) {
|
||||||
|
s = strings.TrimSpace(s)
|
||||||
|
if !strings.HasPrefix(s, prefix) {
|
||||||
|
return Invite{}, fmt.Errorf("not a waste invite (expected 'waste:' prefix)")
|
||||||
|
}
|
||||||
|
b, err := base64.URLEncoding.DecodeString(strings.TrimPrefix(s, prefix))
|
||||||
|
if err != nil {
|
||||||
|
return Invite{}, fmt.Errorf("invalid invite: %w", err)
|
||||||
|
}
|
||||||
|
var inv Invite
|
||||||
|
if err := json.Unmarshal(b, &inv); err != nil {
|
||||||
|
return Invite{}, fmt.Errorf("invalid invite payload: %w", err)
|
||||||
|
}
|
||||||
|
if inv.Anchor == "" || inv.Network == "" {
|
||||||
|
return Invite{}, fmt.Errorf("invite is missing anchor or network name")
|
||||||
|
}
|
||||||
|
return inv, nil
|
||||||
|
}
|
||||||
38
internal/invite/invite_test.go
Normal file
38
internal/invite/invite_test.go
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
package invite
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestRoundTrip(t *testing.T) {
|
||||||
|
s, err := Encode("ws://my-vps:17339/ws", "friends")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Encode: %v", err)
|
||||||
|
}
|
||||||
|
if len(s) < 7 || s[:6] != "waste:" {
|
||||||
|
t.Fatalf("expected waste: prefix, got %q", s)
|
||||||
|
}
|
||||||
|
|
||||||
|
inv, err := Decode(s)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Decode: %v", err)
|
||||||
|
}
|
||||||
|
if inv.Anchor != "ws://my-vps:17339/ws" {
|
||||||
|
t.Fatalf("anchor = %q", inv.Anchor)
|
||||||
|
}
|
||||||
|
if inv.Network != "friends" {
|
||||||
|
t.Fatalf("network = %q", inv.Network)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecodeInvalid(t *testing.T) {
|
||||||
|
cases := []string{
|
||||||
|
"",
|
||||||
|
"notaninvite",
|
||||||
|
"waste:!!!",
|
||||||
|
"waste:" + "eyJhbmNob3IiOiIifQ==", // missing network
|
||||||
|
}
|
||||||
|
for _, c := range cases {
|
||||||
|
if _, err := Decode(c); err == nil {
|
||||||
|
t.Errorf("Decode(%q) expected error, got nil", c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
"github.com/waste-go/internal/invite"
|
||||||
"github.com/waste-go/internal/mesh"
|
"github.com/waste-go/internal/mesh"
|
||||||
"github.com/waste-go/internal/proto"
|
"github.com/waste-go/internal/proto"
|
||||||
)
|
)
|
||||||
@@ -24,8 +25,9 @@ import (
|
|||||||
type JoinFunc func(ctx context.Context, networkName string)
|
type JoinFunc func(ctx context.Context, networkName string)
|
||||||
|
|
||||||
// Run starts the IPC listener. Blocks until the listener fails.
|
// Run starts the IPC listener. Blocks until the listener fails.
|
||||||
|
// anchorURL is the configured anchor WebSocket URL (used for invite generation).
|
||||||
// Network join/leave state is daemon-scoped (shared across all IPC clients).
|
// Network join/leave state is daemon-scoped (shared across all IPC clients).
|
||||||
func Run(m *mesh.Mesh, port int, join JoinFunc) error {
|
func Run(m *mesh.Mesh, port int, anchorURL string, join JoinFunc) error {
|
||||||
addr := fmt.Sprintf("127.0.0.1:%d", port)
|
addr := fmt.Sprintf("127.0.0.1:%d", port)
|
||||||
ln, err := net.Listen("tcp", addr)
|
ln, err := net.Listen("tcp", addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -33,11 +35,12 @@ func Run(m *mesh.Mesh, port int, join JoinFunc) error {
|
|||||||
}
|
}
|
||||||
log.Printf("ipc: listening on %s", addr)
|
log.Printf("ipc: listening on %s", addr)
|
||||||
|
|
||||||
// networkCancel is shared across all clients — any client can join/leave,
|
// networkCancel / networkName are shared across all clients — any client
|
||||||
// and the join persists even after the commanding client disconnects.
|
// can join/leave, and the join persists after the commanding client disconnects.
|
||||||
var (
|
var (
|
||||||
networkMu sync.Mutex
|
networkMu sync.Mutex
|
||||||
networkCancel context.CancelFunc
|
networkCancel context.CancelFunc
|
||||||
|
networkName string
|
||||||
)
|
)
|
||||||
|
|
||||||
doJoin := func(name string) {
|
doJoin := func(name string) {
|
||||||
@@ -47,6 +50,7 @@ func Run(m *mesh.Mesh, port int, join JoinFunc) error {
|
|||||||
}
|
}
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
networkCancel = cancel
|
networkCancel = cancel
|
||||||
|
networkName = name
|
||||||
networkMu.Unlock()
|
networkMu.Unlock()
|
||||||
go join(ctx, name)
|
go join(ctx, name)
|
||||||
}
|
}
|
||||||
@@ -56,21 +60,28 @@ func Run(m *mesh.Mesh, port int, join JoinFunc) error {
|
|||||||
if networkCancel != nil {
|
if networkCancel != nil {
|
||||||
networkCancel()
|
networkCancel()
|
||||||
networkCancel = nil
|
networkCancel = nil
|
||||||
|
networkName = ""
|
||||||
}
|
}
|
||||||
networkMu.Unlock()
|
networkMu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
currentNetwork := func() string {
|
||||||
|
networkMu.Lock()
|
||||||
|
defer networkMu.Unlock()
|
||||||
|
return networkName
|
||||||
|
}
|
||||||
|
|
||||||
for {
|
for {
|
||||||
conn, err := ln.Accept()
|
conn, err := ln.Accept()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("ipc accept: %w", err)
|
return fmt.Errorf("ipc accept: %w", err)
|
||||||
}
|
}
|
||||||
log.Printf("ipc: UI client connected from %s", conn.RemoteAddr())
|
log.Printf("ipc: UI client connected from %s", conn.RemoteAddr())
|
||||||
go handleClient(conn, m, doJoin, doLeave)
|
go handleClient(conn, m, anchorURL, currentNetwork, doJoin, doLeave)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleClient(conn net.Conn, m *mesh.Mesh, doJoin func(string), doLeave func()) {
|
func handleClient(conn net.Conn, m *mesh.Mesh, anchorURL string, currentNetwork func() string, doJoin func(string), doLeave func()) {
|
||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
|
|
||||||
events := m.Subscribe()
|
events := m.Subscribe()
|
||||||
@@ -187,6 +198,23 @@ func handleClient(conn net.Conn, m *mesh.Mesh, doJoin func(string), doLeave func
|
|||||||
Rooms: []string{"general"},
|
Rooms: []string{"general"},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
case proto.CmdGenerateInvite:
|
||||||
|
net := currentNetwork()
|
||||||
|
if net == "" {
|
||||||
|
send(errMsg("generate_invite: not currently joined to a network"))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if anchorURL == "" {
|
||||||
|
send(errMsg("generate_invite: daemon was started without -anchor flag"))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
inv, err := invite.Encode(anchorURL, net)
|
||||||
|
if err != nil {
|
||||||
|
send(errMsg(fmt.Sprintf("generate_invite: %v", err)))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
send(proto.IpcMessage{Type: proto.EvtInviteGenerated, InviteString: inv})
|
||||||
|
|
||||||
case proto.CmdSendFile:
|
case proto.CmdSendFile:
|
||||||
send(errMsg("file transfer not yet implemented"))
|
send(errMsg("file transfer not yet implemented"))
|
||||||
|
|
||||||
|
|||||||
@@ -186,6 +186,7 @@ const (
|
|||||||
CmdLeaveNetwork IpcMsgType = "leave_network"
|
CmdLeaveNetwork IpcMsgType = "leave_network"
|
||||||
CmdGetState IpcMsgType = "get_state"
|
CmdGetState IpcMsgType = "get_state"
|
||||||
CmdSendFile IpcMsgType = "send_file"
|
CmdSendFile IpcMsgType = "send_file"
|
||||||
|
CmdGenerateInvite IpcMsgType = "generate_invite"
|
||||||
|
|
||||||
// Events (daemon → UI)
|
// Events (daemon → UI)
|
||||||
EvtMessageReceived IpcMsgType = "message_received"
|
EvtMessageReceived IpcMsgType = "message_received"
|
||||||
@@ -196,6 +197,7 @@ const (
|
|||||||
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"
|
||||||
)
|
)
|
||||||
|
|
||||||
// IpcMessage covers both commands and events.
|
// IpcMessage covers both commands and events.
|
||||||
@@ -226,4 +228,5 @@ type IpcMessage struct {
|
|||||||
ConnectedPeers []PeerInfo `json:"connected_peers,omitempty"`
|
ConnectedPeers []PeerInfo `json:"connected_peers,omitempty"`
|
||||||
Rooms []string `json:"rooms,omitempty"`
|
Rooms []string `json:"rooms,omitempty"`
|
||||||
ErrorMessage string `json:"error_message,omitempty"`
|
ErrorMessage string `json:"error_message,omitempty"`
|
||||||
|
InviteString string `json:"invite,omitempty"`
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user