ua-labels: derive source from User-Agent, persist peerID in localStorage
Server now parses the User-Agent header on /api/send when no explicit source is provided, producing labels like 'iphone-safari', 'macos-chrome', 'windows-firefox' so the feed shows where messages came from at a glance. Browser-side peerID is now stored in localStorage instead of being freshly random on each page load — refreshing or reopening the tab keeps the same identity for the same browser profile. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
1
go.mod
1
go.mod
@@ -12,6 +12,7 @@ require (
|
|||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
|
github.com/mileusna/useragent v1.3.5 // indirect
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
github.com/pion/datachannel v1.6.0 // indirect
|
github.com/pion/datachannel v1.6.0 // indirect
|
||||||
github.com/pion/dtls/v3 v3.1.2 // indirect
|
github.com/pion/dtls/v3 v3.1.2 // indirect
|
||||||
|
|||||||
2
go.sum
2
go.sum
@@ -18,6 +18,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
|||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||||
|
github.com/mileusna/useragent v1.3.5 h1:SJM5NzBmh/hO+4LGeATKpaEX9+b4vcGg2qXGLiNGDws=
|
||||||
|
github.com/mileusna/useragent v1.3.5/go.mod h1:3d8TOmwL/5I8pJjyVDteHtgDGcefrFUX4ccGOMKNYYc=
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||||
github.com/pion/datachannel v1.6.0 h1:XecBlj+cvsxhAMZWFfFcPyUaDZtd7IJvrXqlXD/53i0=
|
github.com/pion/datachannel v1.6.0 h1:XecBlj+cvsxhAMZWFfFcPyUaDZtd7IJvrXqlXD/53i0=
|
||||||
|
|||||||
@@ -31,11 +31,45 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/mileusna/useragent"
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// labelFromUA returns a short, human-friendly platform label like
|
||||||
|
// "iphone-safari", "macos-chrome", "windows-firefox". Falls back to "web".
|
||||||
|
func labelFromUA(uaStr string) string {
|
||||||
|
ua := useragent.Parse(uaStr)
|
||||||
|
platform := strings.ToLower(ua.OS)
|
||||||
|
switch {
|
||||||
|
case ua.IsIOS() && strings.Contains(strings.ToLower(ua.Device), "ipad"):
|
||||||
|
platform = "ipad"
|
||||||
|
case ua.IsIOS():
|
||||||
|
platform = "iphone"
|
||||||
|
case ua.IsAndroid():
|
||||||
|
platform = "android"
|
||||||
|
case ua.IsMacOS():
|
||||||
|
platform = "macos"
|
||||||
|
case ua.IsWindows():
|
||||||
|
platform = "windows"
|
||||||
|
case ua.IsLinux():
|
||||||
|
platform = "linux"
|
||||||
|
}
|
||||||
|
browser := strings.ToLower(ua.Name)
|
||||||
|
browser = strings.ReplaceAll(browser, " ", "")
|
||||||
|
if platform == "" && browser == "" {
|
||||||
|
return "web"
|
||||||
|
}
|
||||||
|
if browser == "" {
|
||||||
|
return platform
|
||||||
|
}
|
||||||
|
if platform == "" {
|
||||||
|
return browser
|
||||||
|
}
|
||||||
|
return platform + "-" + browser
|
||||||
|
}
|
||||||
|
|
||||||
//go:embed web
|
//go:embed web
|
||||||
var webFS embed.FS
|
var webFS embed.FS
|
||||||
|
|
||||||
@@ -251,7 +285,7 @@ func main() {
|
|||||||
if m.Source == "" {
|
if m.Source == "" {
|
||||||
m.Source = r.Header.Get("X-Tether-Source")
|
m.Source = r.Header.Get("X-Tether-Source")
|
||||||
if m.Source == "" {
|
if m.Source == "" {
|
||||||
m.Source = "web"
|
m.Source = labelFromUA(r.Header.Get("User-Agent"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
m.TS = time.Now().UnixMilli()
|
m.TS = time.Now().UnixMilli()
|
||||||
|
|||||||
@@ -151,7 +151,12 @@
|
|||||||
if (!roomMatch) { window.location.replace("/"); }
|
if (!roomMatch) { window.location.replace("/"); }
|
||||||
const room = roomMatch ? roomMatch[1] : "";
|
const room = roomMatch ? roomMatch[1] : "";
|
||||||
const fullUrl = window.location.origin + "/r/" + room;
|
const fullUrl = window.location.origin + "/r/" + room;
|
||||||
const peerID = "browser-" + Array.from(crypto.getRandomValues(new Uint8Array(6))).map(b=>b.toString(16).padStart(2,"0")).join("");
|
// Stable per-device id — persists across reloads (unless cleared).
|
||||||
|
let peerID = localStorage.getItem("tether-peerID");
|
||||||
|
if (!peerID) {
|
||||||
|
peerID = "browser-" + Array.from(crypto.getRandomValues(new Uint8Array(6))).map(b=>b.toString(16).padStart(2,"0")).join("");
|
||||||
|
localStorage.setItem("tether-peerID", peerID);
|
||||||
|
}
|
||||||
|
|
||||||
$("roomTag").textContent = room;
|
$("roomTag").textContent = room;
|
||||||
$("sessionUrl").textContent = fullUrl;
|
$("sessionUrl").textContent = fullUrl;
|
||||||
@@ -218,16 +223,16 @@
|
|||||||
const r = await fetch("/api/send?room=" + room, {
|
const r = await fetch("/api/send?room=" + room, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ type: "clipboard", text, source: "web", from: peerID, room }),
|
body: JSON.stringify({ type: "clipboard", text, from: peerID, room }),
|
||||||
});
|
});
|
||||||
status(r.ok ? "delivered ✓" : "server returned " + r.status, r.ok ? "ok" : "err");
|
status(r.ok ? "delivered ✓" : "server returned " + r.status, r.ok ? "ok" : "err");
|
||||||
} catch (e) { status("network error", "err"); }
|
} catch (e) { status("network error", "err"); }
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Signaling ─────────────────────────────────────────────────────────
|
// ── Signaling ─────────────────────────────────────────────────────────
|
||||||
|
// Let the server fill in source from User-Agent unless caller specifies.
|
||||||
async function postMessage(m) {
|
async function postMessage(m) {
|
||||||
m.from = m.from || peerID;
|
m.from = m.from || peerID;
|
||||||
m.source = m.source || "web";
|
|
||||||
m.room = room;
|
m.room = room;
|
||||||
await fetch("/api/send?room=" + room, {
|
await fetch("/api/send?room=" + room, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
|||||||
Reference in New Issue
Block a user