Files
reflector-dashboard/svxdr/statusnorm/statusnorm.go
T

143 lines
3.8 KiB
Go

// svxdr — live-data daemon for Reflector Dashboard (nodelist + proxy + astmeta + xlink)
// Copyright (C) 2026 SA7AUX (Björn)
// SPDX-License-Identifier: AGPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify it under
// the terms of the GNU Affero General Public License as published by the Free
// Software Foundation, either version 3 of the License, or (at your option) any
// later version. See the LICENSE file for details.
// Package statusnorm holds the ONE rule that turns a reflector's shared sat-login
// (e.g. KT4ROY-ASL, used by all its TG nodes) into the per-TG display callsign
// (KT4ROY-ASL1..4). It is applied ONCE where a status is ingested (the proxy's
// cache fill, and the federation crawl), so every consumer downstream reads
// already-correct data — no per-view special-casing anywhere.
package statusnorm
import "encoding/json"
// Normalize rewrites the callsign of every node in trunks[].nodes and
// satellites[].nodes: rename(callsign, tg) returns the replacement, or "" to keep
// it. All other data is preserved. Parse failure or no change -> raw unchanged.
func Normalize(raw []byte, rename func(callsign string, tg int) string) []byte {
var top map[string]json.RawMessage
if json.Unmarshal(raw, &top) != nil {
return raw
}
changed := false
// Top-level nodes[]: a group can present as a plain node (e.g. a DMR gateway),
// not only under a trunk/satellite. Keyed by the node's login/identity, so rename
// by that KEY (its callsign field may be the operator, not the login).
if blk, ok := top["nodes"]; ok {
var nm map[string]json.RawMessage
if json.Unmarshal(blk, &nm) == nil {
blkChanged := false
for id, nRaw := range nm {
var n map[string]json.RawMessage
if json.Unmarshal(nRaw, &n) != nil {
continue
}
cs, tg := callsignTG(n)
repl := rename(id, tg) // match on the node's KEY (its login)
if repl == "" || repl == cs {
continue
}
b, _ := json.Marshal(repl)
n["callsign"] = b
if enc, err := json.Marshal(n); err == nil {
nm[id] = enc
blkChanged = true
}
}
if blkChanged {
if enc, err := json.Marshal(nm); err == nil {
top["nodes"] = enc
changed = true
}
}
}
}
for _, key := range []string{"trunks", "satellites"} {
blk, ok := top[key]
if !ok {
continue
}
var group map[string]json.RawMessage
if json.Unmarshal(blk, &group) != nil {
continue
}
blkChanged := false
for id, objRaw := range group {
var obj map[string]json.RawMessage
if json.Unmarshal(objRaw, &obj) != nil {
continue
}
nodesRaw, ok := obj["nodes"]
if !ok {
continue
}
var nodes []json.RawMessage
if json.Unmarshal(nodesRaw, &nodes) != nil {
continue
}
nodesChanged := false
for i, nRaw := range nodes {
var n map[string]json.RawMessage
if json.Unmarshal(nRaw, &n) != nil {
continue
}
cs, tg := callsignTG(n)
if cs == "" {
continue
}
repl := rename(cs, tg)
if repl == "" || repl == cs {
continue
}
b, _ := json.Marshal(repl)
n["callsign"] = b
if enc, err := json.Marshal(n); err == nil {
nodes[i] = enc
nodesChanged = true
}
}
if nodesChanged {
if enc, err := json.Marshal(nodes); err == nil {
obj["nodes"] = enc
if oenc, err := json.Marshal(obj); err == nil {
group[id] = oenc
blkChanged = true
}
}
}
}
if blkChanged {
if enc, err := json.Marshal(group); err == nil {
top[key] = enc
changed = true
}
}
}
if !changed {
return raw
}
if enc, err := json.Marshal(top); err == nil {
return enc
}
return raw
}
func callsignTG(n map[string]json.RawMessage) (string, int) {
var cs string
if v, ok := n["callsign"]; ok {
_ = json.Unmarshal(v, &cs)
}
var tg int
if v, ok := n["tg"]; ok {
_ = json.Unmarshal(v, &tg)
}
return cs, tg
}