v0.5: stateless room-based sessions with QR pairing
The single shared mesh is replaced by per-session rooms. Visit / and the server mints a random 8-hex-char id, redirects to /r/<id>. That URL IS the session — share the link (or scan the QR code now shown on the page) on another device to join the same room. Bus is now sharded per room. Rooms are created implicitly on first subscribe and GC'd 5 minutes after the last subscriber leaves. No accounts, no persistence, no server-side state beyond the in-memory bus map. Server: - New endpoints: /, /r/<id>, /api/send?room=, /api/stream?room= - Room manager with lazy creation + idle GC - Metrics now labelled by room - New gauge tether_active_rooms Client (Go): - -room flag (accepts bare id OR full /r/<id> URL — paste-friendly) - All API calls now scope to the room - The always-on ct210-rtc-peer systemd unit is disabled — sessions are user-initiated; the user runs tether-client with -room when they want their laptop in a particular session Browser (HTML): - Reads room from /r/<id> path - Shows QR code + URL + "copy link" button at top - "+ new session" link in header to start a fresh room Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -53,6 +53,7 @@ type Message struct {
|
||||
To string `json:"to,omitempty"`
|
||||
Role string `json:"role,omitempty"`
|
||||
Source string `json:"source,omitempty"`
|
||||
Room string `json:"room,omitempty"`
|
||||
TS int64 `json:"ts"`
|
||||
}
|
||||
|
||||
@@ -68,16 +69,31 @@ var (
|
||||
myLabel string
|
||||
useRTC bool
|
||||
rtcServer string
|
||||
myRoom string
|
||||
)
|
||||
|
||||
func main() {
|
||||
server := flag.String("server", "https://tether.pecord.io", "tether-server base URL")
|
||||
label := flag.String("label", "", "X-Tether-Client label (default: <os>-sse-<role>)")
|
||||
sendText := flag.String("send", "", "send this text and exit (otherwise listen)")
|
||||
roomFlag := flag.String("room", "", "room id to join (or full URL like https://tether.pecord.io/r/<id>)")
|
||||
flag.BoolVar(&noClipboard, "no-clipboard", false, "don't write incoming messages to the OS clipboard")
|
||||
flag.BoolVar(&useRTC, "rtc", false, "act as a WebRTC mesh peer")
|
||||
flag.Parse()
|
||||
|
||||
// Accept either bare id or full URL
|
||||
myRoom = *roomFlag
|
||||
if strings.Contains(myRoom, "/r/") {
|
||||
parts := strings.SplitN(myRoom, "/r/", 2)
|
||||
if len(parts) == 2 {
|
||||
myRoom = strings.SplitN(parts[1], "/", 2)[0]
|
||||
}
|
||||
}
|
||||
if myRoom == "" {
|
||||
fmt.Fprintln(os.Stderr, "tether-client: -room is required (e.g. -room abc12345 or paste the /r/<id> URL)")
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
if *label == "" {
|
||||
if *sendText != "" {
|
||||
*label = defaultLabel("sender")
|
||||
@@ -128,8 +144,11 @@ func sendMessage(server, label string, m Message) {
|
||||
}
|
||||
|
||||
func postMessage(server, label string, m Message) {
|
||||
if m.Room == "" {
|
||||
m.Room = myRoom
|
||||
}
|
||||
body, _ := json.Marshal(m)
|
||||
req, _ := http.NewRequest("POST", server+"/api/send", bytes.NewReader(body))
|
||||
req, _ := http.NewRequest("POST", server+"/api/send?room="+myRoom, bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("X-Tether-Source", label)
|
||||
r, err := http.DefaultClient.Do(req)
|
||||
@@ -149,7 +168,7 @@ func postMessage(server, label string, m Message) {
|
||||
|
||||
// listen subscribes to the SSE stream and dispatches messages to handlers.
|
||||
func listen(server, label string) error {
|
||||
req, _ := http.NewRequest("GET", server+"/api/stream", nil)
|
||||
req, _ := http.NewRequest("GET", server+"/api/stream?room="+myRoom, nil)
|
||||
req.Header.Set("X-Tether-Client", label)
|
||||
r, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user