// svxdr — live-data daemon for SvxPortal (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 }