diff --git a/go.mod b/go.mod index 744fdbc..1ac6fee 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/jackc/pgx/v5 v5.10.0 github.com/meshcore-go/meshcore-go v1.0.8 github.com/pressly/goose/v3 v3.27.1 + github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e github.com/testcontainers/testcontainers-go/modules/postgres v0.43.0 golang.org/x/crypto v0.53.0 ) diff --git a/go.sum b/go.sum index 898ce1a..0185eab 100644 --- a/go.sum +++ b/go.sum @@ -153,6 +153,8 @@ github.com/shirou/gopsutil/v4 v4.26.5 h1:RPcBXkpz7kOj9PqGFQOlBPZHsyaPvPVQc098y9R github.com/shirou/gopsutil/v4 v4.26.5/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ= github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= +github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= +github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= diff --git a/internal/core/repeaters.go b/internal/core/repeaters.go index c1263a8..1a7da5f 100644 --- a/internal/core/repeaters.go +++ b/internal/core/repeaters.go @@ -1,13 +1,18 @@ package core import ( + "encoding/base64" "errors" "fmt" + "html/template" + "image/color" "net/http" + "net/url" "strconv" "strings" meshcore "github.com/meshcore-go/meshcore-go" + qrcode "github.com/skip2/go-qrcode" "github.com/jleight/meshtender/internal/config" "github.com/jleight/meshtender/internal/store" @@ -47,11 +52,24 @@ func (s *Handlers) pageRepeater(w http.ResponseWriter, r *http.Request) { isOwner := !rep.Shared radio := fmt.Sprintf("%g MHz / %g kHz / SF%d / CR%d", float64(rep.RadioFreqHz)/1e6, float64(rep.RadioBwHz)/1e3, rep.RadioSF, rep.RadioCR) + contactURI := repeaterContactURI(rep) data := map[string]any{ - "Repeater": rep, - "IsOwner": isOwner, - "Radio": radio, - "Error": r.URL.Query().Get("error"), + "Repeater": rep, + "IsOwner": isOwner, + "Radio": radio, + "ContactURI": contactURI, + "Error": r.URL.Query().Get("error"), + } + // QR code that adds the repeater as a MeshCore contact. Embedded as a data + // URI so it needs no extra route or asset; if encoding fails the page just + // renders without it. Light modules on a transparent quiet zone so it sits on + // the dark card instead of a stark white block (scanners decode inverted QR). + if qr, err := qrcode.New(contactURI, qrcode.Medium); err == nil { + qr.BackgroundColor = color.Transparent + qr.ForegroundColor = color.RGBA{R: 0x8a, G: 0x97, B: 0xa8, A: 0xff} + if png, err := qr.PNG(256); err == nil { + data["ContactQR"] = template.URL("data:image/png;base64," + base64.StdEncoding.EncodeToString(png)) + } } if isOwner { orgs, err := s.Store.ListRepeaterOrgs(r.Context(), id) @@ -70,6 +88,16 @@ func (s *Handlers) pageRepeater(w http.ResponseWriter, r *http.Request) { s.Render(w, r, "repeater.html", data) } +// repeaterContactURI builds the meshcore:// deep link that adds the repeater as +// a contact in the MeshCore app. type=2 is MeshCore's repeater contact type. +func repeaterContactURI(rep *store.Repeater) string { + q := url.Values{} + q.Set("name", rep.Name) + q.Set("public_key", rep.PublicKeyHex) + q.Set("type", "2") + return "meshcore://contact/add?" + q.Encode() +} + func addErr(w http.ResponseWriter, r *http.Request, msg string) { web.RedirectErr(w, r, "/repeaters/add?step=details", msg) } diff --git a/internal/core/templates/repeater.html b/internal/core/templates/repeater.html index f6fa65a..6dfe82c 100644 --- a/internal/core/templates/repeater.html +++ b/internal/core/templates/repeater.html @@ -38,16 +38,6 @@
{{.Repeater.PublicKeyHex}}
- setperm <your key> 3 on the repeater to grant admin, then re-test.
@@ -86,6 +81,29 @@
Not contributed to any organization.
{{.Repeater.PublicKeyHex}}
+
+ Scan with the MeshCore app to add this repeater as a contact.
+Not contributed to any organization.