init
This commit is contained in:
@@ -0,0 +1,673 @@
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/example/notify-gateway/internal/config"
|
||||
"github.com/example/notify-gateway/internal/gateway"
|
||||
"github.com/example/notify-gateway/internal/model"
|
||||
)
|
||||
|
||||
type editableConfig struct {
|
||||
Version int `json:"version"`
|
||||
Server editableServerConfig `json:"server"`
|
||||
Divera config.DiveraConfig `json:"divera"`
|
||||
Ingress config.IngressConfig `json:"ingress"`
|
||||
Mappings []config.Mapping `json:"mappings"`
|
||||
Outbounds []config.OutboundConfig `json:"outbounds,omitempty"`
|
||||
}
|
||||
|
||||
type editableServerConfig struct {
|
||||
Listen string `json:"listen"`
|
||||
AdminUsername string `json:"admin_username"`
|
||||
}
|
||||
|
||||
func makeEditable(c config.Config) editableConfig {
|
||||
return editableConfig{
|
||||
Version: c.Version,
|
||||
Server: editableServerConfig{Listen: c.Server.Listen, AdminUsername: c.Server.AdminUsername},
|
||||
Divera: c.Divera,
|
||||
Ingress: c.Ingress,
|
||||
Mappings: c.Mappings,
|
||||
Outbounds: c.Outbounds,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) apiConfig(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
writeJSON(w, http.StatusOK, makeEditable(s.store.Get()))
|
||||
case http.MethodPut:
|
||||
var in editableConfig
|
||||
dec := json.NewDecoder(io.LimitReader(r.Body, 2<<20))
|
||||
dec.DisallowUnknownFields()
|
||||
if err := dec.Decode(&in); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]any{"error": "Ungültige Konfiguration: " + err.Error()})
|
||||
return
|
||||
}
|
||||
old := s.store.Get()
|
||||
next := old
|
||||
next.Version = config.CurrentVersion
|
||||
next.Server.Listen = strings.TrimSpace(in.Server.Listen)
|
||||
next.Server.AdminUsername = strings.TrimSpace(in.Server.AdminUsername)
|
||||
next.Divera = in.Divera
|
||||
next.Ingress = in.Ingress
|
||||
next.Mappings = in.Mappings
|
||||
next.Outbounds = in.Outbounds
|
||||
if err := config.Validate(next); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]any{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if err := s.store.Replace(next); err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]any{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{"ok": true, "config": makeEditable(s.store.Get())})
|
||||
default:
|
||||
w.Header().Set("Allow", "GET, PUT")
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) apiDiveraCatalog(w http.ResponseWriter, r *http.Request) {
|
||||
// Divera247 v2 pull/all is the primary catalog source. Besides the active
|
||||
// unit it returns data.ucr, i.e. every UserClusterRelation the current
|
||||
// account can switch to. We use those UCR ids to load each reachable unit
|
||||
// and merge its cluster.consumer/group/vehicle catalog. This works with
|
||||
// normal v2 access even when the v3 synchronization API is not enabled.
|
||||
resp, err := s.divera.PullAll(r.Context(), nil)
|
||||
if err != nil {
|
||||
status := http.StatusBadGateway
|
||||
if strings.Contains(strings.ToLower(err.Error()), "access_key") {
|
||||
status = http.StatusBadRequest
|
||||
}
|
||||
writeJSON(w, status, map[string]any{"error": err.Error(), "status_code": resp.StatusCode, "response": string(resp.Body)})
|
||||
return
|
||||
}
|
||||
var raw any
|
||||
if err := json.Unmarshal(resp.Body, &raw); err != nil {
|
||||
writeJSON(w, http.StatusBadGateway, map[string]any{"error": "Divera247-Antwort ist kein JSON: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
catalog := extractCatalog(raw)
|
||||
refs := extractUCRRefs(raw)
|
||||
activeUCR := extractActiveUCR(raw)
|
||||
for _, ref := range refs {
|
||||
catalog.Units = mergeCatalogItems(catalog.Units, []catalogItem{{ID: ref.ClusterID, Label: ref.Label}})
|
||||
}
|
||||
if ref, ok := findUCRRef(refs, activeUCR); ok {
|
||||
annotateCatalogWithUnit(&catalog, ref)
|
||||
}
|
||||
|
||||
warnings := []string{}
|
||||
v2Calls, v2Successes := 1, 1
|
||||
failedV2 := []string{}
|
||||
|
||||
// pull/all requests are independent. Limit concurrency so a PRO account with
|
||||
// many units does not create a burst of dozens of simultaneous requests.
|
||||
type pullResult struct {
|
||||
ref ucrRef
|
||||
catalog diveraCatalog
|
||||
err error
|
||||
}
|
||||
resultCh := make(chan pullResult, len(refs))
|
||||
sem := make(chan struct{}, 6)
|
||||
var wg sync.WaitGroup
|
||||
for _, ref := range refs {
|
||||
if ref.UCRID <= 0 || ref.UCRID == activeUCR {
|
||||
continue
|
||||
}
|
||||
ref := ref
|
||||
v2Calls++
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
select {
|
||||
case sem <- struct{}{}:
|
||||
defer func() { <-sem }()
|
||||
case <-r.Context().Done():
|
||||
resultCh <- pullResult{ref: ref, err: r.Context().Err()}
|
||||
return
|
||||
}
|
||||
q := url.Values{}
|
||||
q.Set("ucr", strconv.FormatInt(ref.UCRID, 10))
|
||||
unitResp, unitErr := s.divera.PullAll(r.Context(), q)
|
||||
if unitErr != nil {
|
||||
resultCh <- pullResult{ref: ref, err: unitErr}
|
||||
return
|
||||
}
|
||||
var unitRaw any
|
||||
if err := json.Unmarshal(unitResp.Body, &unitRaw); err != nil {
|
||||
resultCh <- pullResult{ref: ref, err: fmt.Errorf("ungültiges JSON: %w", err)}
|
||||
return
|
||||
}
|
||||
unitCatalog := extractCatalog(unitRaw)
|
||||
annotateCatalogWithUnit(&unitCatalog, ref)
|
||||
resultCh <- pullResult{ref: ref, catalog: unitCatalog}
|
||||
}()
|
||||
}
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(resultCh)
|
||||
}()
|
||||
for result := range resultCh {
|
||||
if result.err != nil {
|
||||
failedV2 = append(failedV2, fmt.Sprintf("%s: %v", result.ref.Label, result.err))
|
||||
continue
|
||||
}
|
||||
v2Successes++
|
||||
mergeDiveraCatalog(&catalog, result.catalog)
|
||||
}
|
||||
if len(failedV2) > 0 {
|
||||
examples := failedV2
|
||||
if len(examples) > 3 {
|
||||
examples = examples[:3]
|
||||
}
|
||||
warnings = append(warnings, fmt.Sprintf("Stammdaten für %d von %d Divera247-Einheiten konnten über v2 pull/all nicht geladen werden. Beispiele: %s", len(failedV2), len(refs), strings.Join(examples, " | ")))
|
||||
}
|
||||
|
||||
personSource := "v2 pull/all · cluster.consumer"
|
||||
v3Calls, v3Successes := 0, 0
|
||||
|
||||
// v3 is a beta/synchronization API and can legitimately be forbidden for an
|
||||
// otherwise valid access key. Only use it as a fallback when v2 did not yield
|
||||
// any consumers. On an explicit 403 stop immediately instead of producing one
|
||||
// warning per unit.
|
||||
if len(catalog.Persons) == 0 {
|
||||
personSource = "keine Personen verfügbar"
|
||||
probeRefs := refs
|
||||
if len(probeRefs) == 0 {
|
||||
probeRefs = []ucrRef{{}}
|
||||
}
|
||||
v3Persons := []catalogItem{}
|
||||
for _, ref := range probeRefs {
|
||||
var clusterID *int64
|
||||
if ref.ClusterID > 0 {
|
||||
id := ref.ClusterID
|
||||
clusterID = &id
|
||||
}
|
||||
v3Calls++
|
||||
userResp, userErr := s.divera.ListUserClusterRelations(r.Context(), clusterID)
|
||||
if userErr != nil {
|
||||
if userResp.StatusCode == http.StatusForbidden {
|
||||
warnings = append(warnings, "Divera247 v3 Benutzer-Synchronisation ist für diesen Accesskey nicht freigeschaltet (HTTP 403). Personen werden ausschließlich aus v2 pull/all gelesen.")
|
||||
break
|
||||
}
|
||||
warnings = append(warnings, "Divera247-v3-Fallback für Personen fehlgeschlagen: "+userErr.Error())
|
||||
break
|
||||
}
|
||||
persons, parseErr := parseV3Persons(userResp.Body)
|
||||
if parseErr != nil {
|
||||
warnings = append(warnings, "Divera247-v3-Benutzerantwort konnte nicht gelesen werden: "+parseErr.Error())
|
||||
break
|
||||
}
|
||||
v3Successes++
|
||||
v3Persons = mergeCatalogItems(v3Persons, persons)
|
||||
}
|
||||
if len(v3Persons) > 0 {
|
||||
catalog.Persons = v3Persons
|
||||
personSource = "v3 user-cluster-relations"
|
||||
}
|
||||
}
|
||||
|
||||
sortCatalog(&catalog)
|
||||
writeJSON(w, http.StatusOK, map[string]any{
|
||||
"ok": true,
|
||||
"catalog": catalog,
|
||||
"warnings": warnings,
|
||||
"diagnostics": map[string]any{
|
||||
"v2_ucr_calls": v2Calls,
|
||||
"v2_ucr_successes": v2Successes,
|
||||
"v3_user_calls": v3Calls,
|
||||
"v3_user_successes": v3Successes,
|
||||
"person_count": len(catalog.Persons),
|
||||
"person_source": personSource,
|
||||
"available_ucr_count": len(refs),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
type ucrRef struct {
|
||||
UCRID int64
|
||||
ClusterID int64
|
||||
Label string
|
||||
}
|
||||
|
||||
func extractUCRRefs(raw any) []ucrRef {
|
||||
root, ok := raw.(map[string]any)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
data, ok := root["data"].(map[string]any)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
var refs []ucrRef
|
||||
add := func(m map[string]any, implicit int64) {
|
||||
ucrID, _ := positiveInt64(m["id"])
|
||||
if ucrID <= 0 {
|
||||
ucrID = implicit
|
||||
}
|
||||
clusterID, _ := positiveInt64(m["cluster_id"])
|
||||
if ucrID <= 0 || clusterID <= 0 {
|
||||
return
|
||||
}
|
||||
label := anyString(m["name"])
|
||||
short := anyString(m["shortname"])
|
||||
if label == "" {
|
||||
label = short
|
||||
} else if short != "" && short != label {
|
||||
label += " (" + short + ")"
|
||||
}
|
||||
if label == "" {
|
||||
label = fmt.Sprintf("Einheit %d", clusterID)
|
||||
}
|
||||
refs = append(refs, ucrRef{UCRID: ucrID, ClusterID: clusterID, Label: label})
|
||||
}
|
||||
switch u := data["ucr"].(type) {
|
||||
case []any:
|
||||
for _, row := range u {
|
||||
if m, ok := row.(map[string]any); ok {
|
||||
add(m, 0)
|
||||
}
|
||||
}
|
||||
case map[string]any:
|
||||
for key, row := range u {
|
||||
if m, ok := row.(map[string]any); ok {
|
||||
implicit, _ := strconv.ParseInt(key, 10, 64)
|
||||
add(m, implicit)
|
||||
}
|
||||
}
|
||||
}
|
||||
byUCR := map[int64]ucrRef{}
|
||||
for _, ref := range refs {
|
||||
byUCR[ref.UCRID] = ref
|
||||
}
|
||||
refs = refs[:0]
|
||||
for _, ref := range byUCR {
|
||||
refs = append(refs, ref)
|
||||
}
|
||||
sort.Slice(refs, func(i, j int) bool {
|
||||
li, lj := strings.ToLower(refs[i].Label), strings.ToLower(refs[j].Label)
|
||||
if li == lj {
|
||||
return refs[i].UCRID < refs[j].UCRID
|
||||
}
|
||||
return li < lj
|
||||
})
|
||||
return refs
|
||||
}
|
||||
|
||||
func extractActiveUCR(raw any) int64 {
|
||||
root, ok := raw.(map[string]any)
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
data, ok := root["data"].(map[string]any)
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
id, _ := positiveInt64(data["ucr_active"])
|
||||
return id
|
||||
}
|
||||
|
||||
func findUCRRef(refs []ucrRef, id int64) (ucrRef, bool) {
|
||||
for _, ref := range refs {
|
||||
if ref.UCRID == id {
|
||||
return ref, true
|
||||
}
|
||||
}
|
||||
return ucrRef{}, false
|
||||
}
|
||||
|
||||
func annotateCatalogWithUnit(c *diveraCatalog, ref ucrRef) {
|
||||
if ref.ClusterID <= 0 {
|
||||
return
|
||||
}
|
||||
unit := ref.Label
|
||||
if unit == "" {
|
||||
unit = fmt.Sprintf("Einheit %d", ref.ClusterID)
|
||||
}
|
||||
suffix := fmt.Sprintf("Einheit: %s · Cluster-ID %d", unit, ref.ClusterID)
|
||||
annotate := func(items []catalogItem) {
|
||||
for i := range items {
|
||||
if !strings.Contains(items[i].Subtitle, "Cluster-ID ") {
|
||||
if items[i].Subtitle == "" {
|
||||
items[i].Subtitle = suffix
|
||||
} else {
|
||||
items[i].Subtitle += " · " + suffix
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
annotate(c.Groups)
|
||||
annotate(c.Persons)
|
||||
annotate(c.Vehicles)
|
||||
}
|
||||
|
||||
func mergeDiveraCatalog(dst *diveraCatalog, src diveraCatalog) {
|
||||
dst.Units = mergeCatalogItems(dst.Units, src.Units)
|
||||
dst.Groups = mergeCatalogItems(dst.Groups, src.Groups)
|
||||
dst.Persons = mergeCatalogItems(dst.Persons, src.Persons)
|
||||
dst.Vehicles = mergeCatalogItems(dst.Vehicles, src.Vehicles)
|
||||
}
|
||||
|
||||
type catalogItem struct {
|
||||
ID int64 `json:"id"`
|
||||
Label string `json:"label"`
|
||||
Subtitle string `json:"subtitle,omitempty"`
|
||||
}
|
||||
|
||||
type diveraCatalog struct {
|
||||
Units []catalogItem `json:"units"`
|
||||
Groups []catalogItem `json:"groups"`
|
||||
Persons []catalogItem `json:"persons"`
|
||||
Vehicles []catalogItem `json:"vehicles"`
|
||||
}
|
||||
|
||||
// extractCatalog tolerates different pull/all response layouts. Numeric keys in
|
||||
// collections are preserved separately because in cluster/consumer Divera247 can use
|
||||
// that key as UCR id while the nested object's "id" is the global user id.
|
||||
func extractCatalog(raw any) diveraCatalog {
|
||||
out := diveraCatalog{}
|
||||
seen := map[string]map[int64]bool{
|
||||
"units": {}, "groups": {}, "persons": {}, "vehicles": {},
|
||||
}
|
||||
var walk func(v any, path string)
|
||||
walk = func(v any, path string) {
|
||||
switch x := v.(type) {
|
||||
case map[string]any:
|
||||
kind := classifyCatalogPath(path)
|
||||
if id, ok := objectIDForKind(x, kind); ok && kind != "" && !seen[kind][id] {
|
||||
seen[kind][id] = true
|
||||
item := catalogItem{ID: id, Label: objectLabel(x, id), Subtitle: objectSubtitle(x)}
|
||||
switch kind {
|
||||
case "units":
|
||||
out.Units = append(out.Units, item)
|
||||
case "groups":
|
||||
out.Groups = append(out.Groups, item)
|
||||
case "persons":
|
||||
out.Persons = append(out.Persons, item)
|
||||
case "vehicles":
|
||||
out.Vehicles = append(out.Vehicles, item)
|
||||
}
|
||||
}
|
||||
for k, child := range x {
|
||||
p := strings.ToLower(k)
|
||||
if path != "" {
|
||||
p = path + "/" + p
|
||||
}
|
||||
// pull/all commonly returns collections as objects keyed by numeric ID.
|
||||
// Preserve the key even if the child has its own "id" field: for users,
|
||||
// the child id may be the global User-ID while the collection key is UCR.
|
||||
if cm, ok := child.(map[string]any); ok {
|
||||
if implicit, err := strconv.ParseInt(k, 10, 64); err == nil && implicit > 0 {
|
||||
copyMap := make(map[string]any, len(cm)+1)
|
||||
for ck, cv := range cm {
|
||||
copyMap[ck] = cv
|
||||
}
|
||||
copyMap["__collection_key_id"] = implicit
|
||||
child = copyMap
|
||||
}
|
||||
}
|
||||
walk(child, p)
|
||||
}
|
||||
case []any:
|
||||
for _, child := range x {
|
||||
walk(child, path)
|
||||
}
|
||||
}
|
||||
}
|
||||
walk(raw, "")
|
||||
sortCatalog(&out)
|
||||
return out
|
||||
}
|
||||
|
||||
func classifyCatalogPath(path string) string {
|
||||
p := strings.ToLower(path)
|
||||
switch {
|
||||
case strings.Contains(p, "vehicle") || strings.Contains(p, "fahrzeug"):
|
||||
return "vehicles"
|
||||
case strings.Contains(p, "user_cluster_relation") || strings.Contains(p, "user-cluster-relation") || strings.Contains(p, "userclusterrelation") || strings.Contains(p, "consumer") || strings.Contains(p, "member") || strings.Contains(p, "person") || strings.Contains(p, "/user") || strings.HasSuffix(p, "users"):
|
||||
return "persons"
|
||||
case strings.Contains(p, "group") || strings.Contains(p, "gruppen"):
|
||||
return "groups"
|
||||
case strings.Contains(p, "cluster") || strings.Contains(p, "unit") || strings.Contains(p, "standort") || strings.Contains(p, "einheit"):
|
||||
return "units"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func objectIDForKind(m map[string]any, kind string) (int64, bool) {
|
||||
if kind == "persons" {
|
||||
// Prefer explicit UCR fields. If pull/all uses cluster/consumer keyed by UCR,
|
||||
// prefer the collection key before the nested global user id.
|
||||
for _, k := range []string{"user_cluster_relation_id", "userClusterRelationId", "ucr_id", "ucr"} {
|
||||
if id, ok := positiveInt64(m[k]); ok {
|
||||
return id, true
|
||||
}
|
||||
}
|
||||
if id, ok := positiveInt64(m["__collection_key_id"]); ok {
|
||||
return id, true
|
||||
}
|
||||
}
|
||||
for _, k := range []string{"id", "cluster_id", "group_id", "vehicle_id", "__collection_key_id"} {
|
||||
if id, ok := positiveInt64(m[k]); ok {
|
||||
return id, true
|
||||
}
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func positiveInt64(v any) (int64, bool) {
|
||||
switch n := v.(type) {
|
||||
case int64:
|
||||
return n, n > 0
|
||||
case int:
|
||||
return int64(n), n > 0
|
||||
case float64:
|
||||
return int64(n), n > 0
|
||||
case json.Number:
|
||||
i, err := n.Int64()
|
||||
return i, err == nil && i > 0
|
||||
case string:
|
||||
i, err := strconv.ParseInt(strings.TrimSpace(n), 10, 64)
|
||||
return i, err == nil && i > 0
|
||||
default:
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
|
||||
func objectLabel(m map[string]any, id int64) string {
|
||||
for _, k := range []string{"title", "name", "display_name", "fullname", "full_name", "label", "shortname", "callname", "number"} {
|
||||
if s := anyString(m[k]); s != "" {
|
||||
return s
|
||||
}
|
||||
}
|
||||
first := anyString(m["firstname"])
|
||||
last := anyString(m["lastname"])
|
||||
if strings.TrimSpace(first+" "+last) != "" {
|
||||
return strings.TrimSpace(first + " " + last)
|
||||
}
|
||||
if nested, ok := m["user"].(map[string]any); ok {
|
||||
first = anyString(nested["firstname"])
|
||||
last = anyString(nested["lastname"])
|
||||
if strings.TrimSpace(first+" "+last) != "" {
|
||||
return strings.TrimSpace(first + " " + last)
|
||||
}
|
||||
if email := anyString(nested["email"]); email != "" {
|
||||
return email
|
||||
}
|
||||
}
|
||||
if email := anyString(m["email"]); email != "" {
|
||||
return email
|
||||
}
|
||||
return fmt.Sprintf("ID %d", id)
|
||||
}
|
||||
|
||||
func objectSubtitle(m map[string]any) string {
|
||||
vals := []string{}
|
||||
appendUnique := func(s string) {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
return
|
||||
}
|
||||
for _, old := range vals {
|
||||
if old == s {
|
||||
return
|
||||
}
|
||||
}
|
||||
vals = append(vals, s)
|
||||
}
|
||||
for _, k := range []string{"foreign_id", "ric", "issi", "opta", "number", "email"} {
|
||||
appendUnique(anyString(m[k]))
|
||||
}
|
||||
if nested, ok := m["user"].(map[string]any); ok {
|
||||
appendUnique(anyString(nested["email"]))
|
||||
}
|
||||
if clusterID, ok := positiveInt64(m["cluster_id"]); ok {
|
||||
appendUnique(fmt.Sprintf("Einheit %d", clusterID))
|
||||
}
|
||||
return strings.Join(vals, " · ")
|
||||
}
|
||||
|
||||
func anyString(v any) string {
|
||||
switch x := v.(type) {
|
||||
case string:
|
||||
return strings.TrimSpace(x)
|
||||
case int:
|
||||
return strconv.Itoa(x)
|
||||
case int64:
|
||||
return strconv.FormatInt(x, 10)
|
||||
case float64:
|
||||
return strconv.FormatInt(int64(x), 10)
|
||||
case json.Number:
|
||||
return x.String()
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func parseV3Persons(body []byte) ([]catalogItem, error) {
|
||||
var rows []struct {
|
||||
ID int64 `json:"id"`
|
||||
ClusterID int64 `json:"cluster_id"`
|
||||
ForeignID string `json:"foreign_id"`
|
||||
User struct {
|
||||
ID int64 `json:"id"`
|
||||
Firstname string `json:"firstname"`
|
||||
Lastname string `json:"lastname"`
|
||||
Email string `json:"email"`
|
||||
} `json:"user"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &rows); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items := make([]catalogItem, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
if row.ID <= 0 {
|
||||
continue
|
||||
}
|
||||
label := strings.TrimSpace(row.User.Firstname + " " + row.User.Lastname)
|
||||
if label == "" {
|
||||
label = strings.TrimSpace(row.User.Email)
|
||||
}
|
||||
if label == "" {
|
||||
label = fmt.Sprintf("UCR %d", row.ID)
|
||||
}
|
||||
parts := []string{}
|
||||
if row.User.Email != "" {
|
||||
parts = append(parts, row.User.Email)
|
||||
}
|
||||
if row.ClusterID > 0 {
|
||||
parts = append(parts, fmt.Sprintf("Einheit %d", row.ClusterID))
|
||||
}
|
||||
if row.ForeignID != "" {
|
||||
parts = append(parts, row.ForeignID)
|
||||
}
|
||||
items = append(items, catalogItem{ID: row.ID, Label: label, Subtitle: strings.Join(parts, " · ")})
|
||||
}
|
||||
return mergeCatalogItems(nil, items), nil
|
||||
}
|
||||
|
||||
func mergeCatalogItems(base, add []catalogItem) []catalogItem {
|
||||
byID := make(map[int64]catalogItem, len(base)+len(add))
|
||||
for _, item := range base {
|
||||
if item.ID > 0 {
|
||||
byID[item.ID] = item
|
||||
}
|
||||
}
|
||||
for _, item := range add {
|
||||
if item.ID <= 0 {
|
||||
continue
|
||||
}
|
||||
if old, ok := byID[item.ID]; ok {
|
||||
// Prefer the richer v3/person label over generic fallback labels.
|
||||
if strings.HasPrefix(old.Label, "ID ") || strings.HasPrefix(old.Label, "UCR ") || len(item.Label) > len(old.Label) {
|
||||
old.Label = item.Label
|
||||
}
|
||||
if item.Subtitle != "" {
|
||||
old.Subtitle = item.Subtitle
|
||||
}
|
||||
byID[item.ID] = old
|
||||
} else {
|
||||
byID[item.ID] = item
|
||||
}
|
||||
}
|
||||
out := make([]catalogItem, 0, len(byID))
|
||||
for _, item := range byID {
|
||||
out = append(out, item)
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool {
|
||||
li, lj := strings.ToLower(out[i].Label), strings.ToLower(out[j].Label)
|
||||
if li == lj {
|
||||
return out[i].ID < out[j].ID
|
||||
}
|
||||
return li < lj
|
||||
})
|
||||
return out
|
||||
}
|
||||
|
||||
func sortCatalog(c *diveraCatalog) {
|
||||
sorter := func(items []catalogItem) {
|
||||
sort.Slice(items, func(i, j int) bool {
|
||||
li, lj := strings.ToLower(items[i].Label), strings.ToLower(items[j].Label)
|
||||
if li == lj {
|
||||
return items[i].ID < items[j].ID
|
||||
}
|
||||
return li < lj
|
||||
})
|
||||
}
|
||||
sorter(c.Units)
|
||||
sorter(c.Groups)
|
||||
sorter(c.Persons)
|
||||
sorter(c.Vehicles)
|
||||
}
|
||||
|
||||
type previewRequest struct {
|
||||
Mapping config.Mapping `json:"mapping"`
|
||||
Message model.InboundMessage `json:"message"`
|
||||
}
|
||||
|
||||
func (s *Server) apiPreview(w http.ResponseWriter, r *http.Request) {
|
||||
var in previewRequest
|
||||
if err := json.NewDecoder(io.LimitReader(r.Body, 1<<20)).Decode(&in); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]any{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
payload, kind, matched, err := gateway.Preview(in.Mapping, in.Message)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]any{"error": err.Error(), "matched": matched})
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{"matched": matched, "kind": kind, "payload": payload})
|
||||
}
|
||||
Reference in New Issue
Block a user