Files
meshcore-analyzer/cmd/server/observer_mail.go
efitenandClaude Opus 5.5 b65412ab83 feat(observer-logins): turn on neighbour reports in the firmware block of the credentials mail (#2349)
The firmware block of the observer credentials mail now ends with `set
mqtt.neighbors on`, so firmware observers send the same `/neighbors`
report that the OpenHop block already turns on ("Publish Neighbours",
#2347). The ingestor reads that report for region scopes (#1865).

## Changes

- `cmd/server/observer_mail.go`: one extra line in the firmware block,
and one sentence in its note. The sentence says the command sends the
neighbour table and region scopes once a day, and that older firmware
does not know it and should skip it.
- `cmd/server/observer_mail_test.go`: the full firmware block assertion
includes the new line, and the slot test checks the line and the note.
- `docs/observer-mqtt-logins.md`: the mail section describes the new
line.

## Tests

- `go test ./...` in `cmd/server`: ok. Skipped
`TestSaveGeoFilterPreservesFileMode`, which needs Unix file modes; this
run was on Windows.
- `TestObserverCredentialsMail*` failed before the change, on the
missing line, and passes after it.

## Not checked

- The command name and the 24-hour default come from #1865. I did not
read the observer firmware source.
- The wording of the error older firmware gives for an unknown command
is not quoted in the mail. The note only says to skip the line.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 06:25:42 +01:00

126 lines
5.1 KiB
Go

package main
import (
"strconv"
"github.com/meshcore-analyzer/mailer"
"github.com/meshcore-analyzer/users"
)
// observerCredentialsPurpose is the mail_log purpose and the mail tag of the
// credentials mail.
const observerCredentialsPurpose = "observer-credentials"
// observerCredentialsMail carries the login in five formats. It is the only
// place the password leaves the server, and it is never logged: mail_log
// keeps only the purpose and the provider message id.
func (a *authService) observerCredentialsMail(u *users.User, username, password string) mailer.Message {
o := a.set.observerLogins
port := strconv.Itoa(o.port)
tlsOn, tlsWord := "false", "no"
if o.tls {
tlsOn, tlsWord = "true", "yes"
}
meshcoretomqtt := "[[broker]]\n" +
"name = \"" + o.host + "\"\n" +
"enabled = true\n" +
"server = \"" + o.host + "\"\n" +
"port = " + port + "\n" +
"transport = \"" + o.transport + "\"\n" +
"keepalive = 60\n" +
"qos = 0\n" +
"retain = true\n" +
"[broker.tls]\n" +
"enabled = " + tlsOn + "\n" +
"[broker.auth]\n" +
"method = \"password\"\n" +
"username = \"" + username + "\"\n" +
"password = \"" + password + "\""
firmware := "set mqtt4.preset custom\n" +
"set mqtt4.server " + firmwareServer(o) + "\n" +
"set mqtt4.port " + port + "\n" +
"set mqtt4.username " + username + "\n" +
"set mqtt4.password " + password + "\n" +
"set mqtt.iata <your IATA code>\n" +
"set mqtt.neighbors on"
tlsSwitch := "off"
if o.tls {
tlsSwitch = "on"
}
homeAssistant := "Enabled: on\n" +
"Server: " + o.host + "\n" +
"Port: " + port + "\n" +
"Transport: " + o.transport + "\n" +
"Use TLS: " + tlsSwitch + "\n" +
"Use MeshCore Auth Token: off\n" +
"Username: " + username + "\n" +
"Password: " + password + "\n" +
"Broker IATA Code: <your IATA code>"
openHopTransport := "TCP"
if o.transport == "websockets" {
openHopTransport = "Websockets"
}
openHop := "Format: Letsmesh\n" +
"Transport: " + openHopTransport + "\n" +
"Host: " + o.host + "\n" +
"Port: " + port + "\n" +
"Username: " + username + "\n" +
"Password: " + password + "\n" +
"TLS: " + tlsSwitch + "\n" +
"Use JWT Auth: off\n" +
"Retain Status: on\n" +
"Publish Neighbours: on"
plain := "Host: " + o.host + "\n" +
"Port: " + port + "\n" +
"TLS: " + tlsWord + "\n" +
"Username: " + username + "\n" +
"Password: " + password
return a.render(u.Email, u.DisplayName, observerCredentialsPurpose, mailContent{
subject: "Your observer MQTT login",
greeting: "Hello " + u.DisplayName + ",",
paragraphs: []string{
"Below is your personal MQTT login for " + a.set.baseURL.Host + ", in five formats: the meshcoretomqtt config, the MeshCore observer firmware commands (Gessaman or Dutch-MeshCore), the Home Assistant (meshcore-ha) form fields, the OpenHop broker form, and the plain values.",
"One login serves all your observers. Each observer is linked to you the first time it publishes with this login. If you lose this mail, ask a new copy from your account page.",
},
pre: []mailBlock{
{title: "1. meshcoretomqtt (uploader on a Raspberry Pi or Linux box)",
note: "Add this block to your meshcoretomqtt config file, next to any brokers already in it, and restart the service.", body: meshcoretomqtt},
{title: "2. MeshCore observer firmware (Gessaman or Dutch-MeshCore)",
note: "Type these commands one by one on the serial console, replace <your IATA code> with your area code, then reboot. They use slot 4, which is empty on a fresh install; if get mqtt.status shows it in use, replace mqtt4 with an empty slot. The last command sends your node's neighbour table and region scopes once a day, which this analyzer reads; older firmware does not know it, skip it there.", body: firmware},
{title: "3. Home Assistant (meshcore-ha)",
note: "Settings > Devices & services > MeshCore > Configure > Manage MQTT Brokers > add a broker, then fill in these fields.", body: homeAssistant},
{title: "4. OpenHop (repeater software)",
note: "In the OpenHop web UI, Configuration > Add Broker, fill in these fields. Format Meshcoretomqtt works too, but not \"openHop\": that one publishes to a topic this broker refuses. Publish Neighbours sends your node's neighbour table and region scopes, which this analyzer reads.", body: openHop},
{title: "5. Any other MQTT client",
note: "The plain values.", body: plain},
},
actionLabel: "Your account", actionURL: a.set.baseURL.String() + "/#/account?section=observers",
})
}
// firmwareServer is the mqttN.server value for the observer firmware.
// For a bare host the firmware picks the scheme from the port alone (8883
// mqtts, 443 wss, anything else mqtt) and appends mqttN.port itself. Firmware
// before the merge 32dd8ad (2026-10-19) does not accept a scheme in the
// server, so the host stays bare unless the port would pick the wrong scheme.
func firmwareServer(o observerLoginSettings) string {
scheme := "mqtt"
if o.transport == "websockets" {
scheme = "ws"
}
if o.tls {
scheme += "s"
}
inferred := "mqtt"
switch o.port {
case 8883:
inferred = "mqtts"
case 443:
inferred = "wss"
}
if scheme == inferred {
return o.host
}
return scheme + "://" + o.host
}