New tg ID schema and initial importer #35

Merged
amigan merged 17 commits from reid into trunk 2024-11-15 13:28:05 -05:00
15 changed files with 113 additions and 96 deletions
Showing only changes of commit fb1b6a475c - Show all commits

View file

@ -7,7 +7,7 @@ import (
"text/template" "text/template"
"time" "time"
"dynatron.me/x/stillbox/internal/jsontime" "dynatron.me/x/stillbox/internal/jsontypes"
) )
var ( var (
@ -27,7 +27,7 @@ var (
} }
return dict, nil return dict, nil
}, },
"formTime": func(t jsontime.Time) string { "formTime": func(t jsontypes.Time) string {
return time.Time(t).Format("2006-01-02T15:04") return time.Time(t).Format("2006-01-02T15:04")
}, },
"ago": func(s string) (string, error) { "ago": func(s string) (string, error) {

View file

@ -11,7 +11,7 @@ import (
"strings" "strings"
"time" "time"
"dynatron.me/x/stillbox/internal/jsontime" "dynatron.me/x/stillbox/internal/jsontypes"
"github.com/araddon/dateparse" "github.com/araddon/dateparse"
) )
@ -262,13 +262,13 @@ func (o *options) iterFields(r *http.Request, destStruct reflect.Value) error {
return err return err
} }
setVal(destFieldVal, set, val) setVal(destFieldVal, set, val)
case time.Time, *time.Time, jsontime.Time, *jsontime.Time: case time.Time, *time.Time, jsontypes.Time, *jsontypes.Time:
t, set, err := o.parseTime(ff) t, set, err := o.parseTime(ff)
if err != nil { if err != nil {
return err return err
} }
setVal(destFieldVal, set, t) setVal(destFieldVal, set, t)
case time.Duration, *time.Duration, jsontime.Duration, *jsontime.Duration: case time.Duration, *time.Duration, jsontypes.Duration, *jsontypes.Duration:
d, set, err := o.parseDuration(ff) d, set, err := o.parseDuration(ff)
if err != nil { if err != nil {
return err return err

View file

@ -10,7 +10,7 @@ import (
"dynatron.me/x/stillbox/internal/common" "dynatron.me/x/stillbox/internal/common"
"dynatron.me/x/stillbox/internal/forms" "dynatron.me/x/stillbox/internal/forms"
"dynatron.me/x/stillbox/internal/jsontime" "dynatron.me/x/stillbox/internal/jsontypes"
"dynatron.me/x/stillbox/pkg/alerting" "dynatron.me/x/stillbox/pkg/alerting"
"dynatron.me/x/stillbox/pkg/config" "dynatron.me/x/stillbox/pkg/config"
@ -48,19 +48,19 @@ type urlEncTest struct {
} }
type urlEncTestJT struct { type urlEncTestJT struct {
LookbackDays uint `json:"lookbackDays"` LookbackDays uint `json:"lookbackDays"`
HalfLife jsontime.Duration `json:"halfLife"` HalfLife jsontypes.Duration `json:"halfLife"`
Recent string `json:"recent"` Recent string `json:"recent"`
ScoreStart jsontime.Time `json:"scoreStart"` ScoreStart jsontypes.Time `json:"scoreStart"`
ScoreEnd jsontime.Time `json:"scoreEnd"` ScoreEnd jsontypes.Time `json:"scoreEnd"`
} }
type ptrTestJT struct { type ptrTestJT struct {
LookbackDays uint `form:"lookbackDays"` LookbackDays uint `form:"lookbackDays"`
HalfLife *jsontime.Duration `form:"halfLife"` HalfLife *jsontypes.Duration `form:"halfLife"`
Recent *string `form:"recent"` Recent *string `form:"recent"`
ScoreStart *jsontime.Time `form:"scoreStart"` ScoreStart *jsontypes.Time `form:"scoreStart"`
ScoreEnd jsontime.Time `form:"scoreEnd"` ScoreEnd jsontypes.Time `form:"scoreEnd"`
} }
var ( var (
@ -73,33 +73,33 @@ var (
UrlEncTestJT = urlEncTestJT{ UrlEncTestJT = urlEncTestJT{
LookbackDays: 7, LookbackDays: 7,
HalfLife: jsontime.Duration(30 * time.Minute), HalfLife: jsontypes.Duration(30 * time.Minute),
Recent: "2h0m0s", Recent: "2h0m0s",
ScoreStart: jsontime.Time(time.Date(2024, time.October, 28, 9, 25, 0, 0, time.UTC)), ScoreStart: jsontypes.Time(time.Date(2024, time.October, 28, 9, 25, 0, 0, time.UTC)),
} }
PtrTestJT = ptrTestJT{ PtrTestJT = ptrTestJT{
LookbackDays: 7, LookbackDays: 7,
HalfLife: common.PtrTo(jsontime.Duration(30 * time.Minute)), HalfLife: common.PtrTo(jsontypes.Duration(30 * time.Minute)),
Recent: common.PtrTo("2h0m0s"), Recent: common.PtrTo("2h0m0s"),
ScoreStart: common.PtrTo(jsontime.Time(time.Date(2024, time.October, 28, 9, 25, 0, 0, time.UTC))), ScoreStart: common.PtrTo(jsontypes.Time(time.Date(2024, time.October, 28, 9, 25, 0, 0, time.UTC))),
} }
UrlEncTestJTLocal = urlEncTestJT{ UrlEncTestJTLocal = urlEncTestJT{
LookbackDays: 7, LookbackDays: 7,
HalfLife: jsontime.Duration(30 * time.Minute), HalfLife: jsontypes.Duration(30 * time.Minute),
Recent: "2h0m0s", Recent: "2h0m0s",
ScoreStart: jsontime.Time(time.Date(2024, time.October, 28, 9, 25, 0, 0, time.Local)), ScoreStart: jsontypes.Time(time.Date(2024, time.October, 28, 9, 25, 0, 0, time.Local)),
} }
realSim = &alerting.Simulation{ realSim = &alerting.Simulation{
Alerting: config.Alerting{ Alerting: config.Alerting{
LookbackDays: 7, LookbackDays: 7,
HalfLife: jsontime.Duration(30 * time.Minute), HalfLife: jsontypes.Duration(30 * time.Minute),
Recent: jsontime.Duration(2 * time.Hour), Recent: jsontypes.Duration(2 * time.Hour),
}, },
SimInterval: jsontime.Duration(5 * time.Minute), SimInterval: jsontypes.Duration(5 * time.Minute),
ScoreStart: jsontime.Time(time.Date(2024, time.October, 22, 17, 49, 0, 0, time.Local)), ScoreStart: jsontypes.Time(time.Date(2024, time.October, 22, 17, 49, 0, 0, time.Local)),
} }
Call1 = callUploadRequest{ Call1 = callUploadRequest{
@ -194,7 +194,7 @@ func TestUnmarshal(t *testing.T) {
opts: []forms.Option{forms.WithAcceptBlank()}, opts: []forms.Option{forms.WithAcceptBlank()},
}, },
{ {
name: "url encoded jsontime", name: "url encoded jsontypes",
r: makeRequest("urlenc.http"), r: makeRequest("urlenc.http"),
dest: &urlEncTestJT{}, dest: &urlEncTestJT{},
expect: &UrlEncTestJT, expect: &UrlEncTestJT,
@ -202,14 +202,14 @@ func TestUnmarshal(t *testing.T) {
opts: []forms.Option{forms.WithTag("json")}, opts: []forms.Option{forms.WithTag("json")},
}, },
{ {
name: "url encoded jsontime with tz", name: "url encoded jsontypes with tz",
r: makeRequest("urlenc.http"), r: makeRequest("urlenc.http"),
dest: &urlEncTestJT{}, dest: &urlEncTestJT{},
expect: &UrlEncTestJT, expect: &UrlEncTestJT,
opts: []forms.Option{forms.WithAcceptBlank(), forms.WithParseTimeInTZ(time.UTC), forms.WithTag("json")}, opts: []forms.Option{forms.WithAcceptBlank(), forms.WithParseTimeInTZ(time.UTC), forms.WithTag("json")},
}, },
{ {
name: "url encoded jsontime with local", name: "url encoded jsontypes with local",
r: makeRequest("urlenc.http"), r: makeRequest("urlenc.http"),
dest: &urlEncTestJT{}, dest: &urlEncTestJT{},
expect: &UrlEncTestJTLocal, expect: &UrlEncTestJTLocal,

View file

@ -1,4 +1,4 @@
package jsontime package jsontypes
import ( import (
"encoding/json" "encoding/json"

View file

@ -0,0 +1,3 @@
package jsontypes
type Metadata map[string]interface{}

View file

@ -9,7 +9,7 @@ import (
"time" "time"
"dynatron.me/x/stillbox/internal/forms" "dynatron.me/x/stillbox/internal/forms"
"dynatron.me/x/stillbox/internal/jsontime" "dynatron.me/x/stillbox/internal/jsontypes"
"dynatron.me/x/stillbox/internal/trending" "dynatron.me/x/stillbox/internal/trending"
"dynatron.me/x/stillbox/pkg/config" "dynatron.me/x/stillbox/pkg/config"
"dynatron.me/x/stillbox/pkg/talkgroups" "dynatron.me/x/stillbox/pkg/talkgroups"
@ -23,12 +23,12 @@ type Simulation struct {
config.Alerting config.Alerting
// ScoreStart is the time when scoring begins // ScoreStart is the time when scoring begins
ScoreStart jsontime.Time `json:"scoreStart" yaml:"scoreStart" form:"scoreStart"` ScoreStart jsontypes.Time `json:"scoreStart" yaml:"scoreStart" form:"scoreStart"`
// ScoreEnd is the time when the score simulator ends. Left blank, it defaults to time.Now() // ScoreEnd is the time when the score simulator ends. Left blank, it defaults to time.Now()
ScoreEnd jsontime.Time `json:"scoreEnd" yaml:"scoreEnd" form:"scoreEnd"` ScoreEnd jsontypes.Time `json:"scoreEnd" yaml:"scoreEnd" form:"scoreEnd"`
// SimInterval is the interval at which the scorer will be called // SimInterval is the interval at which the scorer will be called
SimInterval jsontime.Duration `json:"simInterval" yaml:"simInterval" form:"simInterval"` SimInterval jsontypes.Duration `json:"simInterval" yaml:"simInterval" form:"simInterval"`
clock offsetClock `json:"-"` clock offsetClock `json:"-"`
*alerter `json:"-"` *alerter `json:"-"`
@ -64,7 +64,7 @@ func (s *Simulation) Simulate(ctx context.Context) (trending.Scores[talkgroups.I
s.Enable = true s.Enable = true
s.alerter = New(s.Alerting, tgc, WithClock(&s.clock)).(*alerter) s.alerter = New(s.Alerting, tgc, WithClock(&s.clock)).(*alerter)
if time.Time(s.ScoreEnd).IsZero() { if time.Time(s.ScoreEnd).IsZero() {
s.ScoreEnd = jsontime.Time(now) s.ScoreEnd = jsontypes.Time(now)
} }
log.Debug().Time("scoreStart", s.ScoreStart.Time()). log.Debug().Time("scoreStart", s.ScoreStart.Time()).
Time("scoreEnd", s.ScoreEnd.Time()). Time("scoreEnd", s.ScoreEnd.Time()).

View file

@ -5,7 +5,7 @@ import (
"sync" "sync"
"time" "time"
"dynatron.me/x/stillbox/internal/jsontime" "dynatron.me/x/stillbox/internal/jsontypes"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -54,12 +54,12 @@ type RateLimit struct {
} }
type Alerting struct { type Alerting struct {
Enable bool `yaml:"enable" form:"enable"` Enable bool `yaml:"enable" form:"enable"`
LookbackDays uint `yaml:"lookbackDays" form:"lookbackDays"` LookbackDays uint `yaml:"lookbackDays" form:"lookbackDays"`
HalfLife jsontime.Duration `yaml:"halfLife" form:"halfLife"` HalfLife jsontypes.Duration `yaml:"halfLife" form:"halfLife"`
Recent jsontime.Duration `yaml:"recent" form:"recent"` Recent jsontypes.Duration `yaml:"recent" form:"recent"`
AlertThreshold float64 `yaml:"alertThreshold" form:"alertThreshold"` AlertThreshold float64 `yaml:"alertThreshold" form:"alertThreshold"`
Renotify *jsontime.Duration `yaml:"renotify,omitempty" form:"renotify,omitempty"` Renotify *jsontypes.Duration `yaml:"renotify,omitempty" form:"renotify,omitempty"`
} }
type Notify []NotifyService type Notify []NotifyService

View file

@ -7,6 +7,7 @@ package database
import ( import (
"time" "time"
"dynatron.me/x/stillbox/internal/jsontypes"
"dynatron.me/x/stillbox/pkg/alerting/rules" "dynatron.me/x/stillbox/pkg/alerting/rules"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/jackc/pgx/v5/pgtype" "github.com/jackc/pgx/v5/pgtype"
@ -82,18 +83,18 @@ type System struct {
} }
type Talkgroup struct { type Talkgroup struct {
ID int64 `json:"id"` ID int64 `json:"id"`
SystemID int32 `json:"system_id"` SystemID int32 `json:"system_id"`
Tgid int32 `json:"tgid"` Tgid int32 `json:"tgid"`
Name *string `json:"name"` Name *string `json:"name"`
AlphaTag *string `json:"alpha_tag"` AlphaTag *string `json:"alpha_tag"`
TgGroup *string `json:"tg_group"` TgGroup *string `json:"tg_group"`
Frequency *int32 `json:"frequency"` Frequency *int32 `json:"frequency"`
Metadata []byte `json:"metadata"` Metadata jsontypes.Metadata `json:"metadata"`
Tags []string `json:"tags"` Tags []string `json:"tags"`
Alert bool `json:"alert"` Alert bool `json:"alert"`
AlertConfig rules.AlertRules `json:"alert_config"` AlertConfig rules.AlertRules `json:"alert_config"`
Weight float32 `json:"weight"` Weight float32 `json:"weight"`
} }
type TalkgroupsLearned struct { type TalkgroupsLearned struct {

View file

@ -8,6 +8,7 @@ package database
import ( import (
"context" "context"
"dynatron.me/x/stillbox/internal/jsontypes"
"dynatron.me/x/stillbox/pkg/alerting/rules" "dynatron.me/x/stillbox/pkg/alerting/rules"
) )
@ -492,16 +493,16 @@ RETURNING id, system_id, tgid, name, alpha_tag, tg_group, frequency, metadata, t
` `
type UpdateTalkgroupParams struct { type UpdateTalkgroupParams struct {
Name *string `json:"name"` Name *string `json:"name"`
AlphaTag *string `json:"alpha_tag"` AlphaTag *string `json:"alpha_tag"`
TgGroup *string `json:"tg_group"` TgGroup *string `json:"tg_group"`
Frequency *int32 `json:"frequency"` Frequency *int32 `json:"frequency"`
Metadata []byte `json:"metadata"` Metadata jsontypes.Metadata `json:"metadata"`
Tags []string `json:"tags"` Tags []string `json:"tags"`
Alert *bool `json:"alert"` Alert *bool `json:"alert"`
AlertConfig rules.AlertRules `json:"alert_config"` AlertConfig rules.AlertRules `json:"alert_config"`
Weight *float32 `json:"weight"` Weight *float32 `json:"weight"`
ID int64 `json:"id"` ID int64 `json:"id"`
} }
func (q *Queries) UpdateTalkgroup(ctx context.Context, arg UpdateTalkgroupParams) (Talkgroup, error) { func (q *Queries) UpdateTalkgroup(ctx context.Context, arg UpdateTalkgroupParams) (Talkgroup, error) {

View file

@ -2,7 +2,6 @@ package nexus
import ( import (
"context" "context"
"encoding/json"
"dynatron.me/x/stillbox/pkg/calls" "dynatron.me/x/stillbox/pkg/calls"
"dynatron.me/x/stillbox/pkg/pb" "dynatron.me/x/stillbox/pkg/pb"
@ -70,12 +69,7 @@ func (c *client) Talkgroup(ctx context.Context, tg *pb.Talkgroup) error {
var md *structpb.Struct var md *structpb.Struct
if len(tgi.Talkgroup.Metadata) > 0 { if len(tgi.Talkgroup.Metadata) > 0 {
m := make(map[string]interface{}) md, err = structpb.NewStruct(tgi.Talkgroup.Metadata)
err := json.Unmarshal(tgi.Talkgroup.Metadata, &m)
if err != nil {
log.Error().Err(err).Int32("sys", tg.System).Int32("tg", tg.Talkgroup).Msg("unmarshal tg metadata")
}
md, err = structpb.NewStruct(m)
if err != nil { if err != nil {
log.Error().Err(err).Int32("sys", tg.System).Int32("tg", tg.Talkgroup).Msg("new pb struct for tg metadata") log.Error().Err(err).Int32("sys", tg.System).Int32("tg", tg.Talkgroup).Msg("new pb struct for tg metadata")
} }

View file

@ -42,7 +42,6 @@ type errResponse struct {
func (e *errResponse) Render(w http.ResponseWriter, r *http.Request) error { func (e *errResponse) Render(w http.ResponseWriter, r *http.Request) error {
switch e.Code { switch e.Code {
case http.StatusNotFound:
default: default:
log.Error().Str("path", r.URL.Path).Err(e.Err).Int("code", e.Code).Str("msg", e.Error).Msg("request failed") log.Error().Str("path", r.URL.Path).Err(e.Err).Int("code", e.Code).Str("msg", e.Error).Msg("request failed")
} }
@ -79,8 +78,9 @@ func internalError(err error) render.Renderer {
type errResponder func(error) render.Renderer type errResponder func(error) render.Renderer
var statusMapping = map[error]errResponder{ var statusMapping = map[error]errResponder{
talkgroups.ErrNotFound: recordNotFound, talkgroups.ErrNoSuchSystem: recordNotFound,
pgx.ErrNoRows: recordNotFound, talkgroups.ErrNotFound: recordNotFound,
pgx.ErrNoRows: recordNotFound,
} }
func autoError(err error) render.Renderer { func autoError(err error) render.Renderer {

View file

@ -20,7 +20,7 @@ func (tga *talkgroupAPI) Subrouter() http.Handler {
r.Put("/{system:\\d+}/{id:\\d+}", tga.put) r.Put("/{system:\\d+}/{id:\\d+}", tga.put)
r.Get("/{system:\\d+}/", tga.get) r.Get("/{system:\\d+}/", tga.get)
r.Get("/", tga.get) r.Get("/", tga.get)
r.Put("/import", tga.tgImport) r.Post("/import", tga.tgImport)
return r return r
} }
@ -115,7 +115,7 @@ func (tga *talkgroupAPI) tgImport(w http.ResponseWriter, r *http.Request) {
wErr(w, r, badRequest(err)) wErr(w, r, badRequest(err))
return return
} }
recs, err := impJob.Import() recs, err := impJob.Import(r.Context())
if err != nil { if err != nil {
wErr(w, r, autoError(err)) wErr(w, r, autoError(err))
return return

View file

@ -3,13 +3,14 @@ package talkgroups
import ( import (
"bufio" "bufio"
"bytes" "bytes"
"context"
"errors" "errors"
"io" "io"
"encoding/json"
"regexp" "regexp"
"strconv" "strconv"
"strings" "strings"
"dynatron.me/x/stillbox/internal/jsontypes"
"dynatron.me/x/stillbox/pkg/database" "dynatron.me/x/stillbox/pkg/database"
) )
@ -24,18 +25,18 @@ var (
) )
type importer interface { type importer interface {
importTalkgroups(sys int, r io.Reader) ([]Talkgroup, error) importTalkgroups(ctx context.Context, sys int, r io.Reader) ([]Talkgroup, error)
} }
type ImportJob struct { type ImportJob struct {
Type ImportSource `json:"type"` Type ImportSource `json:"type"`
SystemID int `json:"systemID"` SystemID int `json:"systemID"`
Body string `json:"body"` Body string `json:"body"`
importer `json:"-"` importer `json:"-"`
} }
func (ij *ImportJob) Import() ([]Talkgroup, error) { func (ij *ImportJob) Import(ctx context.Context) ([]Talkgroup, error) {
r := bytes.NewReader([]byte(ij.Body)) r := bytes.NewReader([]byte(ij.Body))
switch ij.Type { switch ij.Type {
@ -44,13 +45,14 @@ func (ij *ImportJob) Import() ([]Talkgroup, error) {
default: default:
return nil, ErrBadImportType return nil, ErrBadImportType
} }
return ij.importTalkgroups(ij.SystemID, r) return ij.importTalkgroups(ctx, ij.SystemID, r)
} }
type radioReferenceImporter struct { type radioReferenceImporter struct {
} }
type rrState int type rrState int
const ( const (
rrsInitial rrState = iota rrsInitial rrState = iota
rrsGroupDesc rrsGroupDesc
@ -59,13 +61,21 @@ const (
var rrRE = regexp.MustCompile(`DEC\s+HEX\s+Mode\s+Alpha Tag\s+Description\s+Tag`) var rrRE = regexp.MustCompile(`DEC\s+HEX\s+Mode\s+Alpha Tag\s+Description\s+Tag`)
func (rr *radioReferenceImporter) importTalkgroups(sys int, r io.Reader) ([]Talkgroup, error) { func (rr *radioReferenceImporter) importTalkgroups(ctx context.Context, sys int, r io.Reader) ([]Talkgroup, error) {
sc := bufio.NewScanner(r) sc := bufio.NewScanner(r)
tgs := make([]Talkgroup, 0, 8) tgs := make([]Talkgroup, 0, 8)
sysn, has := StoreFrom(ctx).SystemName(ctx, sys)
if !has {
return nil, ErrNoSuchSystem
}
var groupName string var groupName string
state := rrsInitial state := rrsInitial
for sc.Scan() { for sc.Scan() {
if err := ctx.Err(); err != nil {
return nil, err
}
ln := strings.Trim(sc.Text(), " \t\r\n") ln := strings.Trim(sc.Text(), " \t\r\n")
switch state { switch state {
@ -87,30 +97,31 @@ func (rr *radioReferenceImporter) importTalkgroups(sys int, r io.Reader) ([]Talk
if err != nil { if err != nil {
continue continue
} }
var metadata []byte var metadata jsontypes.Metadata
tgt := TG(sys, tgid) tgt := TG(sys, tgid)
mode := fields[2] mode := fields[2]
if strings.Contains(mode, "E") { if strings.Contains(mode, "E") {
metadata, _ = json.Marshal(&struct{ metadata = jsontypes.Metadata{
Encrypted bool `json:"encrypted"` "encrypted": true,
}{true}) }
} }
tags := []string{fields[5]} tags := []string{fields[5]}
gn := groupName // must take a copy
tgs = append(tgs, Talkgroup{ tgs = append(tgs, Talkgroup{
Talkgroup: database.Talkgroup{ Talkgroup: database.Talkgroup{
ID: tgt.Pack(), ID: tgt.Pack(),
Tgid: int32(tgt.Talkgroup), Tgid: int32(tgt.Talkgroup),
SystemID: int32(tgt.System), SystemID: int32(tgt.System),
Name: &fields[4], Name: &fields[4],
AlphaTag: &fields[3], AlphaTag: &fields[3],
TgGroup: &groupName, TgGroup: &gn,
Metadata: metadata, Metadata: metadata,
Tags: tags, Tags: tags,
Weight: 1.0, Weight: 1.0,
}, },
System: database.System{ System: database.System{
ID: sys, ID: sys,
Name: "<imported>", Name: sysn,
}, },
}) })

View file

@ -12,6 +12,8 @@ type Talkgroup struct {
Learned bool `json:"learned"` Learned bool `json:"learned"`
} }
type Metadata map[string]interface{}
type Names struct { type Names struct {
System string System string
Talkgroup string Talkgroup string

View file

@ -32,3 +32,8 @@ sql:
import: "dynatron.me/x/stillbox/pkg/alerting/rules" import: "dynatron.me/x/stillbox/pkg/alerting/rules"
type: "AlertRules" type: "AlertRules"
nullable: true nullable: true
- column: "talkgroups.metadata"
go_type:
import: "dynatron.me/x/stillbox/internal/jsontypes"
type: "Metadata"
nullable: true