From f195b6e9b618dc07c94dbd2fafe28b5f586f2e6a Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Tue, 12 Nov 2024 19:20:03 -0500 Subject: [PATCH 01/17] Initial parser --- pkg/rest/talkgroups.go | 17 ++++++ pkg/talkgroups/import.go | 125 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 142 insertions(+) create mode 100644 pkg/talkgroups/import.go diff --git a/pkg/rest/talkgroups.go b/pkg/rest/talkgroups.go index 0520af7..c8f7786 100644 --- a/pkg/rest/talkgroups.go +++ b/pkg/rest/talkgroups.go @@ -20,6 +20,7 @@ func (tga *talkgroupAPI) Subrouter() http.Handler { r.Put("/{system:\\d+}/{id:\\d+}", tga.put) r.Get("/{system:\\d+}/", tga.get) r.Get("/", tga.get) + r.Put("/import", tga.tgImport) return r } @@ -106,3 +107,19 @@ func (tga *talkgroupAPI) put(w http.ResponseWriter, r *http.Request) { respond(w, r, record) } + +func (tga *talkgroupAPI) tgImport(w http.ResponseWriter, r *http.Request) { + var impJob talkgroups.ImportJob + err := forms.Unmarshal(r, &impJob, forms.WithTag("json"), forms.WithAcceptBlank(), forms.WithOmitEmpty()) + if err != nil { + wErr(w, r, badRequest(err)) + return + } + recs, err := impJob.Import() + if err != nil { + wErr(w, r, autoError(err)) + return + } + + respond(w, r, recs) +} diff --git a/pkg/talkgroups/import.go b/pkg/talkgroups/import.go new file mode 100644 index 0000000..6fdf041 --- /dev/null +++ b/pkg/talkgroups/import.go @@ -0,0 +1,125 @@ +package talkgroups + +import ( + "bufio" + "bytes" + "errors" + "io" + "encoding/json" + "regexp" + "strconv" + "strings" + + "dynatron.me/x/stillbox/pkg/database" +) + +type ImportSource string + +const ( + ImportSrcRadioReference ImportSource = "radioreference" +) + +var ( + ErrBadImportType = errors.New("unknown import type") +) + +type importer interface { + importTalkgroups(sys int, r io.Reader) ([]Talkgroup, error) +} + +type ImportJob struct { + Type ImportSource `json:"type"` + SystemID int `json:"systemID"` + Body string `json:"body"` + + importer `json:"-"` +} + +func (ij *ImportJob) Import() ([]Talkgroup, error) { + r := bytes.NewReader([]byte(ij.Body)) + + switch ij.Type { + case ImportSrcRadioReference: + ij.importer = &radioReferenceImporter{} + default: + return nil, ErrBadImportType + } + return ij.importTalkgroups(ij.SystemID, r) +} + +type radioReferenceImporter struct { +} + +type rrState int +const ( + rrsInitial rrState = iota + rrsGroupDesc + rrsTG +) + +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) { + sc := bufio.NewScanner(r) + tgs := make([]Talkgroup, 0, 8) + + var groupName string + state := rrsInitial + for sc.Scan() { + ln := strings.Trim(sc.Text(), " \t\r\n") + + switch state { + case rrsInitial: + groupName = ln + state++ + case rrsGroupDesc: + if rrRE.MatchString(ln) { + state++ + } + case rrsTG: + fields := strings.Split(ln, "\t") + if len(fields) < 6 { + state = rrsGroupDesc + groupName = ln + continue + } + tgid, err := strconv.Atoi(fields[0]) + if err != nil { + continue + } + var metadata []byte + tgt := TG(sys, tgid) + mode := fields[2] + if strings.Contains(mode, "E") { + metadata, _ = json.Marshal(&struct{ + Encrypted bool `json:"encrypted"` + }{true}) + } + tags := []string{fields[5]} + tgs = append(tgs, Talkgroup{ + Talkgroup: database.Talkgroup{ + ID: tgt.Pack(), + Tgid: int32(tgt.Talkgroup), + SystemID: int32(tgt.System), + Name: &fields[4], + AlphaTag: &fields[3], + TgGroup: &groupName, + Metadata: metadata, + Tags: tags, + Weight: 1.0, + }, + System: database.System{ + ID: sys, + Name: "", + }, + }) + + } + } + + if err := sc.Err(); err != nil { + return tgs, err + } + + return tgs, nil +} -- 2.47.0 From fb1b6a475c7442113a754fb7f087abe680430582 Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Wed, 13 Nov 2024 09:24:11 -0500 Subject: [PATCH 02/17] Importer, rename jsontime to jsontypes --- internal/common/template.go | 4 +- internal/forms/forms.go | 6 +-- internal/forms/forms_test.go | 48 +++++++++--------- internal/{jsontime => jsontypes}/jsontime.go | 2 +- internal/jsontypes/metadata.go | 3 ++ pkg/alerting/simulate.go | 10 ++-- pkg/config/config.go | 14 +++--- pkg/database/models.go | 25 +++++----- pkg/database/talkgroups.sql.go | 21 ++++---- pkg/nexus/commands.go | 8 +-- pkg/rest/api.go | 6 +-- pkg/rest/talkgroups.go | 4 +- pkg/talkgroups/import.go | 51 ++++++++++++-------- pkg/talkgroups/talkgroup.go | 2 + sql/sqlc.yaml | 5 ++ 15 files changed, 113 insertions(+), 96 deletions(-) rename internal/{jsontime => jsontypes}/jsontime.go (99%) create mode 100644 internal/jsontypes/metadata.go diff --git a/internal/common/template.go b/internal/common/template.go index f4724c1..8357260 100644 --- a/internal/common/template.go +++ b/internal/common/template.go @@ -7,7 +7,7 @@ import ( "text/template" "time" - "dynatron.me/x/stillbox/internal/jsontime" + "dynatron.me/x/stillbox/internal/jsontypes" ) var ( @@ -27,7 +27,7 @@ var ( } return dict, nil }, - "formTime": func(t jsontime.Time) string { + "formTime": func(t jsontypes.Time) string { return time.Time(t).Format("2006-01-02T15:04") }, "ago": func(s string) (string, error) { diff --git a/internal/forms/forms.go b/internal/forms/forms.go index bc28cc4..f8a7f7a 100644 --- a/internal/forms/forms.go +++ b/internal/forms/forms.go @@ -11,7 +11,7 @@ import ( "strings" "time" - "dynatron.me/x/stillbox/internal/jsontime" + "dynatron.me/x/stillbox/internal/jsontypes" "github.com/araddon/dateparse" ) @@ -262,13 +262,13 @@ func (o *options) iterFields(r *http.Request, destStruct reflect.Value) error { return err } 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) if err != nil { return err } 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) if err != nil { return err diff --git a/internal/forms/forms_test.go b/internal/forms/forms_test.go index 5f73dca..c7108e8 100644 --- a/internal/forms/forms_test.go +++ b/internal/forms/forms_test.go @@ -10,7 +10,7 @@ import ( "dynatron.me/x/stillbox/internal/common" "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/config" @@ -48,19 +48,19 @@ type urlEncTest struct { } type urlEncTestJT struct { - LookbackDays uint `json:"lookbackDays"` - HalfLife jsontime.Duration `json:"halfLife"` - Recent string `json:"recent"` - ScoreStart jsontime.Time `json:"scoreStart"` - ScoreEnd jsontime.Time `json:"scoreEnd"` + LookbackDays uint `json:"lookbackDays"` + HalfLife jsontypes.Duration `json:"halfLife"` + Recent string `json:"recent"` + ScoreStart jsontypes.Time `json:"scoreStart"` + ScoreEnd jsontypes.Time `json:"scoreEnd"` } type ptrTestJT struct { - LookbackDays uint `form:"lookbackDays"` - HalfLife *jsontime.Duration `form:"halfLife"` - Recent *string `form:"recent"` - ScoreStart *jsontime.Time `form:"scoreStart"` - ScoreEnd jsontime.Time `form:"scoreEnd"` + LookbackDays uint `form:"lookbackDays"` + HalfLife *jsontypes.Duration `form:"halfLife"` + Recent *string `form:"recent"` + ScoreStart *jsontypes.Time `form:"scoreStart"` + ScoreEnd jsontypes.Time `form:"scoreEnd"` } var ( @@ -73,33 +73,33 @@ var ( UrlEncTestJT = urlEncTestJT{ LookbackDays: 7, - HalfLife: jsontime.Duration(30 * time.Minute), + HalfLife: jsontypes.Duration(30 * time.Minute), 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{ LookbackDays: 7, - HalfLife: common.PtrTo(jsontime.Duration(30 * time.Minute)), + HalfLife: common.PtrTo(jsontypes.Duration(30 * time.Minute)), 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{ LookbackDays: 7, - HalfLife: jsontime.Duration(30 * time.Minute), + HalfLife: jsontypes.Duration(30 * time.Minute), 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{ Alerting: config.Alerting{ LookbackDays: 7, - HalfLife: jsontime.Duration(30 * time.Minute), - Recent: jsontime.Duration(2 * time.Hour), + HalfLife: jsontypes.Duration(30 * time.Minute), + Recent: jsontypes.Duration(2 * time.Hour), }, - SimInterval: jsontime.Duration(5 * time.Minute), - ScoreStart: jsontime.Time(time.Date(2024, time.October, 22, 17, 49, 0, 0, time.Local)), + SimInterval: jsontypes.Duration(5 * time.Minute), + ScoreStart: jsontypes.Time(time.Date(2024, time.October, 22, 17, 49, 0, 0, time.Local)), } Call1 = callUploadRequest{ @@ -194,7 +194,7 @@ func TestUnmarshal(t *testing.T) { opts: []forms.Option{forms.WithAcceptBlank()}, }, { - name: "url encoded jsontime", + name: "url encoded jsontypes", r: makeRequest("urlenc.http"), dest: &urlEncTestJT{}, expect: &UrlEncTestJT, @@ -202,14 +202,14 @@ func TestUnmarshal(t *testing.T) { opts: []forms.Option{forms.WithTag("json")}, }, { - name: "url encoded jsontime with tz", + name: "url encoded jsontypes with tz", r: makeRequest("urlenc.http"), dest: &urlEncTestJT{}, expect: &UrlEncTestJT, 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"), dest: &urlEncTestJT{}, expect: &UrlEncTestJTLocal, diff --git a/internal/jsontime/jsontime.go b/internal/jsontypes/jsontime.go similarity index 99% rename from internal/jsontime/jsontime.go rename to internal/jsontypes/jsontime.go index 9eb9425..03880e0 100644 --- a/internal/jsontime/jsontime.go +++ b/internal/jsontypes/jsontime.go @@ -1,4 +1,4 @@ -package jsontime +package jsontypes import ( "encoding/json" diff --git a/internal/jsontypes/metadata.go b/internal/jsontypes/metadata.go new file mode 100644 index 0000000..9bda0f6 --- /dev/null +++ b/internal/jsontypes/metadata.go @@ -0,0 +1,3 @@ +package jsontypes + +type Metadata map[string]interface{} diff --git a/pkg/alerting/simulate.go b/pkg/alerting/simulate.go index 0ebefb2..bfe69b7 100644 --- a/pkg/alerting/simulate.go +++ b/pkg/alerting/simulate.go @@ -9,7 +9,7 @@ import ( "time" "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/pkg/config" "dynatron.me/x/stillbox/pkg/talkgroups" @@ -23,12 +23,12 @@ type Simulation struct { config.Alerting // 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 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 jsontime.Duration `json:"simInterval" yaml:"simInterval" form:"simInterval"` + SimInterval jsontypes.Duration `json:"simInterval" yaml:"simInterval" form:"simInterval"` clock offsetClock `json:"-"` *alerter `json:"-"` @@ -64,7 +64,7 @@ func (s *Simulation) Simulate(ctx context.Context) (trending.Scores[talkgroups.I s.Enable = true s.alerter = New(s.Alerting, tgc, WithClock(&s.clock)).(*alerter) if time.Time(s.ScoreEnd).IsZero() { - s.ScoreEnd = jsontime.Time(now) + s.ScoreEnd = jsontypes.Time(now) } log.Debug().Time("scoreStart", s.ScoreStart.Time()). Time("scoreEnd", s.ScoreEnd.Time()). diff --git a/pkg/config/config.go b/pkg/config/config.go index 6a848f9..47e288a 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -5,7 +5,7 @@ import ( "sync" "time" - "dynatron.me/x/stillbox/internal/jsontime" + "dynatron.me/x/stillbox/internal/jsontypes" "github.com/rs/zerolog/log" "github.com/spf13/cobra" @@ -54,12 +54,12 @@ type RateLimit struct { } type Alerting struct { - Enable bool `yaml:"enable" form:"enable"` - LookbackDays uint `yaml:"lookbackDays" form:"lookbackDays"` - HalfLife jsontime.Duration `yaml:"halfLife" form:"halfLife"` - Recent jsontime.Duration `yaml:"recent" form:"recent"` - AlertThreshold float64 `yaml:"alertThreshold" form:"alertThreshold"` - Renotify *jsontime.Duration `yaml:"renotify,omitempty" form:"renotify,omitempty"` + Enable bool `yaml:"enable" form:"enable"` + LookbackDays uint `yaml:"lookbackDays" form:"lookbackDays"` + HalfLife jsontypes.Duration `yaml:"halfLife" form:"halfLife"` + Recent jsontypes.Duration `yaml:"recent" form:"recent"` + AlertThreshold float64 `yaml:"alertThreshold" form:"alertThreshold"` + Renotify *jsontypes.Duration `yaml:"renotify,omitempty" form:"renotify,omitempty"` } type Notify []NotifyService diff --git a/pkg/database/models.go b/pkg/database/models.go index 4d74489..9da62d6 100644 --- a/pkg/database/models.go +++ b/pkg/database/models.go @@ -7,6 +7,7 @@ package database import ( "time" + "dynatron.me/x/stillbox/internal/jsontypes" "dynatron.me/x/stillbox/pkg/alerting/rules" "github.com/google/uuid" "github.com/jackc/pgx/v5/pgtype" @@ -82,18 +83,18 @@ type System struct { } type Talkgroup struct { - ID int64 `json:"id"` - SystemID int32 `json:"system_id"` - Tgid int32 `json:"tgid"` - Name *string `json:"name"` - AlphaTag *string `json:"alpha_tag"` - TgGroup *string `json:"tg_group"` - Frequency *int32 `json:"frequency"` - Metadata []byte `json:"metadata"` - Tags []string `json:"tags"` - Alert bool `json:"alert"` - AlertConfig rules.AlertRules `json:"alert_config"` - Weight float32 `json:"weight"` + ID int64 `json:"id"` + SystemID int32 `json:"system_id"` + Tgid int32 `json:"tgid"` + Name *string `json:"name"` + AlphaTag *string `json:"alpha_tag"` + TgGroup *string `json:"tg_group"` + Frequency *int32 `json:"frequency"` + Metadata jsontypes.Metadata `json:"metadata"` + Tags []string `json:"tags"` + Alert bool `json:"alert"` + AlertConfig rules.AlertRules `json:"alert_config"` + Weight float32 `json:"weight"` } type TalkgroupsLearned struct { diff --git a/pkg/database/talkgroups.sql.go b/pkg/database/talkgroups.sql.go index d511a2b..732b26e 100644 --- a/pkg/database/talkgroups.sql.go +++ b/pkg/database/talkgroups.sql.go @@ -8,6 +8,7 @@ package database import ( "context" + "dynatron.me/x/stillbox/internal/jsontypes" "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 { - Name *string `json:"name"` - AlphaTag *string `json:"alpha_tag"` - TgGroup *string `json:"tg_group"` - Frequency *int32 `json:"frequency"` - Metadata []byte `json:"metadata"` - Tags []string `json:"tags"` - Alert *bool `json:"alert"` - AlertConfig rules.AlertRules `json:"alert_config"` - Weight *float32 `json:"weight"` - ID int64 `json:"id"` + Name *string `json:"name"` + AlphaTag *string `json:"alpha_tag"` + TgGroup *string `json:"tg_group"` + Frequency *int32 `json:"frequency"` + Metadata jsontypes.Metadata `json:"metadata"` + Tags []string `json:"tags"` + Alert *bool `json:"alert"` + AlertConfig rules.AlertRules `json:"alert_config"` + Weight *float32 `json:"weight"` + ID int64 `json:"id"` } func (q *Queries) UpdateTalkgroup(ctx context.Context, arg UpdateTalkgroupParams) (Talkgroup, error) { diff --git a/pkg/nexus/commands.go b/pkg/nexus/commands.go index a14a9ba..502859a 100644 --- a/pkg/nexus/commands.go +++ b/pkg/nexus/commands.go @@ -2,7 +2,6 @@ package nexus import ( "context" - "encoding/json" "dynatron.me/x/stillbox/pkg/calls" "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 if len(tgi.Talkgroup.Metadata) > 0 { - m := make(map[string]interface{}) - 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) + md, err = structpb.NewStruct(tgi.Talkgroup.Metadata) if err != nil { log.Error().Err(err).Int32("sys", tg.System).Int32("tg", tg.Talkgroup).Msg("new pb struct for tg metadata") } diff --git a/pkg/rest/api.go b/pkg/rest/api.go index 86eab07..339ed36 100644 --- a/pkg/rest/api.go +++ b/pkg/rest/api.go @@ -42,7 +42,6 @@ type errResponse struct { func (e *errResponse) Render(w http.ResponseWriter, r *http.Request) error { switch e.Code { - case http.StatusNotFound: default: 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 var statusMapping = map[error]errResponder{ - talkgroups.ErrNotFound: recordNotFound, - pgx.ErrNoRows: recordNotFound, + talkgroups.ErrNoSuchSystem: recordNotFound, + talkgroups.ErrNotFound: recordNotFound, + pgx.ErrNoRows: recordNotFound, } func autoError(err error) render.Renderer { diff --git a/pkg/rest/talkgroups.go b/pkg/rest/talkgroups.go index c8f7786..7185148 100644 --- a/pkg/rest/talkgroups.go +++ b/pkg/rest/talkgroups.go @@ -20,7 +20,7 @@ func (tga *talkgroupAPI) Subrouter() http.Handler { r.Put("/{system:\\d+}/{id:\\d+}", tga.put) r.Get("/{system:\\d+}/", tga.get) r.Get("/", tga.get) - r.Put("/import", tga.tgImport) + r.Post("/import", tga.tgImport) return r } @@ -115,7 +115,7 @@ func (tga *talkgroupAPI) tgImport(w http.ResponseWriter, r *http.Request) { wErr(w, r, badRequest(err)) return } - recs, err := impJob.Import() + recs, err := impJob.Import(r.Context()) if err != nil { wErr(w, r, autoError(err)) return diff --git a/pkg/talkgroups/import.go b/pkg/talkgroups/import.go index 6fdf041..9a59984 100644 --- a/pkg/talkgroups/import.go +++ b/pkg/talkgroups/import.go @@ -3,13 +3,14 @@ package talkgroups import ( "bufio" "bytes" + "context" "errors" "io" - "encoding/json" "regexp" "strconv" "strings" + "dynatron.me/x/stillbox/internal/jsontypes" "dynatron.me/x/stillbox/pkg/database" ) @@ -24,18 +25,18 @@ var ( ) 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 ImportSource `json:"type"` - SystemID int `json:"systemID"` - Body string `json:"body"` + Type ImportSource `json:"type"` + SystemID int `json:"systemID"` + Body string `json:"body"` importer `json:"-"` } -func (ij *ImportJob) Import() ([]Talkgroup, error) { +func (ij *ImportJob) Import(ctx context.Context) ([]Talkgroup, error) { r := bytes.NewReader([]byte(ij.Body)) switch ij.Type { @@ -44,13 +45,14 @@ func (ij *ImportJob) Import() ([]Talkgroup, error) { default: return nil, ErrBadImportType } - return ij.importTalkgroups(ij.SystemID, r) + return ij.importTalkgroups(ctx, ij.SystemID, r) } type radioReferenceImporter struct { } type rrState int + const ( rrsInitial rrState = iota rrsGroupDesc @@ -59,13 +61,21 @@ const ( 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) tgs := make([]Talkgroup, 0, 8) + sysn, has := StoreFrom(ctx).SystemName(ctx, sys) + if !has { + return nil, ErrNoSuchSystem + } var groupName string state := rrsInitial for sc.Scan() { + if err := ctx.Err(); err != nil { + return nil, err + } + ln := strings.Trim(sc.Text(), " \t\r\n") switch state { @@ -87,30 +97,31 @@ func (rr *radioReferenceImporter) importTalkgroups(sys int, r io.Reader) ([]Talk if err != nil { continue } - var metadata []byte + var metadata jsontypes.Metadata tgt := TG(sys, tgid) mode := fields[2] if strings.Contains(mode, "E") { - metadata, _ = json.Marshal(&struct{ - Encrypted bool `json:"encrypted"` - }{true}) + metadata = jsontypes.Metadata{ + "encrypted": true, + } } tags := []string{fields[5]} + gn := groupName // must take a copy tgs = append(tgs, Talkgroup{ Talkgroup: database.Talkgroup{ - ID: tgt.Pack(), - Tgid: int32(tgt.Talkgroup), + ID: tgt.Pack(), + Tgid: int32(tgt.Talkgroup), SystemID: int32(tgt.System), - Name: &fields[4], + Name: &fields[4], AlphaTag: &fields[3], - TgGroup: &groupName, + TgGroup: &gn, Metadata: metadata, - Tags: tags, - Weight: 1.0, + Tags: tags, + Weight: 1.0, }, System: database.System{ - ID: sys, - Name: "", + ID: sys, + Name: sysn, }, }) diff --git a/pkg/talkgroups/talkgroup.go b/pkg/talkgroups/talkgroup.go index e3cca71..b9b746b 100644 --- a/pkg/talkgroups/talkgroup.go +++ b/pkg/talkgroups/talkgroup.go @@ -12,6 +12,8 @@ type Talkgroup struct { Learned bool `json:"learned"` } +type Metadata map[string]interface{} + type Names struct { System string Talkgroup string diff --git a/sql/sqlc.yaml b/sql/sqlc.yaml index 6b617e4..342e2fb 100644 --- a/sql/sqlc.yaml +++ b/sql/sqlc.yaml @@ -32,3 +32,8 @@ sql: import: "dynatron.me/x/stillbox/pkg/alerting/rules" type: "AlertRules" nullable: true + - column: "talkgroups.metadata" + go_type: + import: "dynatron.me/x/stillbox/internal/jsontypes" + type: "Metadata" + nullable: true -- 2.47.0 From 4f18747255f5381d17b63fcaefeace9d5fd2e773 Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Wed, 13 Nov 2024 18:50:26 -0500 Subject: [PATCH 03/17] Add import metadata --- pkg/talkgroups/import.go | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/pkg/talkgroups/import.go b/pkg/talkgroups/import.go index 9a59984..05c8d2e 100644 --- a/pkg/talkgroups/import.go +++ b/pkg/talkgroups/import.go @@ -9,6 +9,7 @@ import ( "regexp" "strconv" "strings" + "time" "dynatron.me/x/stillbox/internal/jsontypes" "dynatron.me/x/stillbox/pkg/database" @@ -41,10 +42,11 @@ func (ij *ImportJob) Import(ctx context.Context) ([]Talkgroup, error) { switch ij.Type { case ImportSrcRadioReference: - ij.importer = &radioReferenceImporter{} + ij.importer = new(radioReferenceImporter) default: return nil, ErrBadImportType } + return ij.importTalkgroups(ctx, ij.SystemID, r) } @@ -69,6 +71,11 @@ func (rr *radioReferenceImporter) importTalkgroups(ctx context.Context, sys int, return nil, ErrNoSuchSystem } + importedFrom := jsontypes.Metadata{ + "from": "RadioReference", + "time": time.Now(), + } + var groupName string state := rrsInitial for sc.Scan() { @@ -88,7 +95,7 @@ func (rr *radioReferenceImporter) importTalkgroups(ctx context.Context, sys int, } case rrsTG: fields := strings.Split(ln, "\t") - if len(fields) < 6 { + if len(fields) != 6 { state = rrsGroupDesc groupName = ln continue @@ -97,13 +104,13 @@ func (rr *radioReferenceImporter) importTalkgroups(ctx context.Context, sys int, if err != nil { continue } - var metadata jsontypes.Metadata + metadata := jsontypes.Metadata{ + "imported": importedFrom, + } tgt := TG(sys, tgid) mode := fields[2] if strings.Contains(mode, "E") { - metadata = jsontypes.Metadata{ - "encrypted": true, - } + metadata["encrypted"] = true } tags := []string{fields[5]} gn := groupName // must take a copy -- 2.47.0 From 18866d893c571b40c4b4e7a45e0d6a79a693c39f Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Wed, 13 Nov 2024 19:10:15 -0500 Subject: [PATCH 04/17] Verbose build --- Makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/Makefile b/Makefile index 86d1ac0..de81f13 100644 --- a/Makefile +++ b/Makefile @@ -2,6 +2,7 @@ VPKG=dynatron.me/x/stillbox/internal/version VER!=git describe --tags --always --dirty BUILDDATE!=date '+%Y%m%d' LDFLAGS=-ldflags="-X '${VPKG}.Version=${VER}' -X '${VPKG}.Built=${BUILDDATE}'" +GOFLAGS=-v all: checkcalls go build -o stillbox ${GOFLAGS} ${LDFLAGS} ./cmd/stillbox/ -- 2.47.0 From 45eb4c9f3ece1d4fc41f2773a98fa951ff5dedc2 Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Wed, 13 Nov 2024 19:40:24 -0500 Subject: [PATCH 05/17] backups --- .bin/dumpdb.sh | 24 ++++++++++++++++++++++++ .gitignore | 1 + Makefile | 7 +++++++ 3 files changed, 32 insertions(+) create mode 100644 .bin/dumpdb.sh diff --git a/.bin/dumpdb.sh b/.bin/dumpdb.sh new file mode 100644 index 0000000..dcc10db --- /dev/null +++ b/.bin/dumpdb.sh @@ -0,0 +1,24 @@ +#!/bin/sh + +config=config.yaml +pgformat="-Fc" +ext=pgdump + +while getopts ":p" arg; do + case $arg in + c) + config=$OPTARG + ;; + p) + pgformat="-Fp" + ext=sql + ;; + esac +done + +filen=`date "+backups/%Y%m%d_%H%M%S.${ext}"` + +mkdir -p backups/ +dbstring=`yq -r .db.connect "${config}"` +pg_dump "${pgformat}" -f "${filen}" -T calls "${dbstring}" +echo "backed up to ${filen}" diff --git a/.gitignore b/.gitignore index 5c93d4c..cb1321f 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ Session.vim *.log *.dlv cover.out +backups/ diff --git a/Makefile b/Makefile index de81f13..997e842 100644 --- a/Makefile +++ b/Makefile @@ -35,5 +35,12 @@ coverage-html: coverage: go test -coverprofile cover.out +# backup backs up the database without calls +backup: + sh .bin/dumpdb.sh + +backupplain: + sh .bin/dumpdb.sh -p + test: go test -v ./... -- 2.47.0 From 9ad1ed17c29f1b20610ceae2e8304a5f4fa097ab Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Wed, 13 Nov 2024 19:58:49 -0500 Subject: [PATCH 06/17] Fix prerun --- Makefile | 3 +++ cmd/stillbox/main.go | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 997e842..675cd29 100644 --- a/Makefile +++ b/Makefile @@ -44,3 +44,6 @@ backupplain: test: go test -v ./... + +run: + go run -v ./cmd/stillbox/ serve diff --git a/cmd/stillbox/main.go b/cmd/stillbox/main.go index b78b84e..95e8284 100644 --- a/cmd/stillbox/main.go +++ b/cmd/stillbox/main.go @@ -24,7 +24,7 @@ func main() { } rootCmd.PersistentFlags().BoolP("version", "V", false, "show version") cfg := config.New(rootCmd) - rootCmd.Run = func(cmd *cobra.Command, args []string) { + rootCmd.PreRun = func(cmd *cobra.Command, args []string) { v, _ := rootCmd.PersistentFlags().GetBool("version") if v { fmt.Print(version.String()) -- 2.47.0 From e38ebe680204b3cc1d3d9b992d9a74e56f99ccc9 Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Thu, 14 Nov 2024 08:37:19 -0500 Subject: [PATCH 07/17] reid first stab --- pkg/alerting/alerting.go | 6 +- pkg/alerting/stats.go | 2 +- pkg/database/models.go | 2 +- pkg/database/querier.go | 8 +- pkg/database/talkgroups.manual.sql.go | 124 ++++++++++++++ pkg/database/talkgroups.sql.go | 131 ++------------- pkg/rest/talkgroups.go | 1 - pkg/talkgroups/cache.go | 18 +- pkg/talkgroups/import.go | 4 +- pkg/talkgroups/talkgroup.go | 19 +-- sql/postgres/migrations/002_reid.down.sql | 9 + sql/postgres/migrations/002_reid.up.sql | 7 + sql/postgres/migrations/flattened_initial.sql | 157 ++++++++++++++++++ sql/postgres/queries/talkgroups.sql | 33 +--- sql/sqlc.yaml | 4 + 15 files changed, 348 insertions(+), 177 deletions(-) create mode 100644 pkg/database/talkgroups.manual.sql.go create mode 100644 sql/postgres/migrations/002_reid.down.sql create mode 100644 sql/postgres/migrations/002_reid.up.sql create mode 100644 sql/postgres/migrations/flattened_initial.sql diff --git a/pkg/alerting/alerting.go b/pkg/alerting/alerting.go index db4c1ff..ee3ec5f 100644 --- a/pkg/alerting/alerting.go +++ b/pkg/alerting/alerting.go @@ -229,10 +229,10 @@ func (as *alerter) scoredTGs() []talkgroups.ID { } // packedScoredTGs gets a list of packed TGIDs. -func (as *alerter) packedScoredTGs() []int64 { - tgs := make([]int64, 0, len(as.scores)) +func (as *alerter) scoredTGsTuple() []database.TalkgroupT { + tgs := make([]database.TalkgroupT, 0, len(as.scores)) for _, s := range as.scores { - tgs = append(tgs, s.ID.Pack()) + tgs = append(tgs, s.ID.Tuple()) } return tgs diff --git a/pkg/alerting/stats.go b/pkg/alerting/stats.go index b724c01..6ba16dd 100644 --- a/pkg/alerting/stats.go +++ b/pkg/alerting/stats.go @@ -40,7 +40,7 @@ func (as *alerter) tgStatsHandler(w http.ResponseWriter, r *http.Request) { ctx := r.Context() db := database.FromCtx(ctx) - tgs, err := db.GetTalkgroupsWithLearnedByPackedIDs(ctx, as.packedScoredTGs()) + tgs, err := db.GetTalkgroupsWithLearnedByPackedIDs(ctx, as.scoredTGsTuple()) if err != nil { log.Error().Err(err).Msg("stats TG get failed") http.Error(w, err.Error(), http.StatusInternalServerError) diff --git a/pkg/database/models.go b/pkg/database/models.go index 9da62d6..8bb2b05 100644 --- a/pkg/database/models.go +++ b/pkg/database/models.go @@ -83,7 +83,7 @@ type System struct { } type Talkgroup struct { - ID int64 `json:"id"` + ID uuid.UUID `json:"id"` SystemID int32 `json:"system_id"` Tgid int32 `json:"tgid"` Name *string `json:"name"` diff --git a/pkg/database/querier.go b/pkg/database/querier.go index 6a814c3..8d91e22 100644 --- a/pkg/database/querier.go +++ b/pkg/database/querier.go @@ -14,7 +14,7 @@ import ( type Querier interface { AddAlert(ctx context.Context, arg AddAlertParams) error AddCall(ctx context.Context, arg AddCallParams) error - BulkSetTalkgroupTags(ctx context.Context, iD int64, tags []string) error + BulkSetTalkgroupTags(ctx context.Context, iD uuid.UUID, tags []string) error CreateAPIKey(ctx context.Context, owner int, expires pgtype.Timestamp, disabled *bool) (ApiKey, error) CreateUser(ctx context.Context, arg CreateUserParams) (User, error) DeleteAPIKey(ctx context.Context, apiKey string) error @@ -22,15 +22,13 @@ type Querier interface { GetAPIKey(ctx context.Context, apiKey string) (ApiKey, error) GetDatabaseSize(ctx context.Context) (string, error) GetSystemName(ctx context.Context, systemID int) (string, error) - GetTalkgroup(ctx context.Context, systemID int, tgid int) (GetTalkgroupRow, error) + GetTalkgroup(ctx context.Context, systemID int32, tgid int32) (GetTalkgroupRow, error) GetTalkgroupIDsByTags(ctx context.Context, anytags []string, alltags []string, nottags []string) ([]GetTalkgroupIDsByTagsRow, error) GetTalkgroupTags(ctx context.Context, sys int, tg int) ([]string, error) - GetTalkgroupWithLearned(ctx context.Context, systemID int, tgid int) (GetTalkgroupWithLearnedRow, error) - GetTalkgroupsByPackedIDs(ctx context.Context, dollar_1 []int64) ([]GetTalkgroupsByPackedIDsRow, error) + GetTalkgroupWithLearned(ctx context.Context, systemID int32, tgid int32) (GetTalkgroupWithLearnedRow, error) GetTalkgroupsWithAllTags(ctx context.Context, tags []string) ([]GetTalkgroupsWithAllTagsRow, error) GetTalkgroupsWithAnyTags(ctx context.Context, tags []string) ([]GetTalkgroupsWithAnyTagsRow, error) GetTalkgroupsWithLearned(ctx context.Context) ([]GetTalkgroupsWithLearnedRow, error) - GetTalkgroupsWithLearnedByPackedIDs(ctx context.Context, dollar_1 []int64) ([]GetTalkgroupsWithLearnedByPackedIDsRow, error) GetTalkgroupsWithLearnedBySystem(ctx context.Context, system int32) ([]GetTalkgroupsWithLearnedBySystemRow, error) GetUserByID(ctx context.Context, id int32) (User, error) GetUserByUID(ctx context.Context, id int32) (User, error) diff --git a/pkg/database/talkgroups.manual.sql.go b/pkg/database/talkgroups.manual.sql.go new file mode 100644 index 0000000..c141e23 --- /dev/null +++ b/pkg/database/talkgroups.manual.sql.go @@ -0,0 +1,124 @@ +package database + +import ( + "context" + "database/sql/driver" +) + +type TalkgroupT struct { + System uint32 `json:"sys"` + Talkgroup uint32 `json:"tg"` +} + +func (t TalkgroupT) Value() (driver.Value, error) { + return [2]uint32{t.System, t.Talkgroup}, nil +} + +const getTalkgroupsWithLearnedByPackedIDs = `-- name: GetTalkgroupsWithLearnedByPackedIDs :many +SELECT +tg.id, tg.system_id, tg.tgid, tg.name, tg.alpha_tag, tg.tg_group, tg.frequency, tg.metadata, tg.tags, tg.alert, tg.alert_config, tg.weight, sys.id, sys.name, +FALSE learned +FROM talkgroups tg +JOIN systems sys ON tg.system_id = sys.id +WHERE (tg.system_id, tg.tgid) = ANY($1) +UNION +SELECT +NULL::UUID, tgl.system_id::INT4, tgl.tgid::INT4, tgl.name, +tgl.alpha_tag, tgl.alpha_tag, NULL::INTEGER, NULL::JSONB, +CASE WHEN tgl.alpha_tag IS NULL THEN NULL ELSE ARRAY[tgl.alpha_tag] END, +TRUE, NULL::JSONB, 1.0, sys.id, sys.name, +TRUE learned +FROM talkgroups_learned tgl +JOIN systems sys ON tgl.system_id = sys.id +WHERE (tgl.system_id, tgl.tgid) = ANY($1); +` + +type GetTalkgroupsWithLearnedByPackedIDsRow struct { + Talkgroup Talkgroup `json:"talkgroup"` + System System `json:"system"` + Learned bool `json:"learned"` +} + +func (q *Queries) GetTalkgroupsWithLearnedByPackedIDs(ctx context.Context, ids []TalkgroupT) ([]GetTalkgroupsWithLearnedByPackedIDsRow, error) { + rows, err := q.db.Query(ctx, getTalkgroupsWithLearnedByPackedIDs, ids) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetTalkgroupsWithLearnedByPackedIDsRow + for rows.Next() { + var i GetTalkgroupsWithLearnedByPackedIDsRow + if err := rows.Scan( + &i.Talkgroup.ID, + &i.Talkgroup.SystemID, + &i.Talkgroup.Tgid, + &i.Talkgroup.Name, + &i.Talkgroup.AlphaTag, + &i.Talkgroup.TgGroup, + &i.Talkgroup.Frequency, + &i.Talkgroup.Metadata, + &i.Talkgroup.Tags, + &i.Talkgroup.Alert, + &i.Talkgroup.AlertConfig, + &i.Talkgroup.Weight, + &i.System.ID, + &i.System.Name, + &i.Learned, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getTalkgroupsByPackedIDs = `-- name: GetTalkgroupsByPackedIDs :many +SELECT tg.id, tg.system_id, tg.tgid, tg.name, tg.alpha_tag, tg.tg_group, tg.frequency, tg.metadata, tg.tags, tg.alert, tg.alert_config, tg.weight, sys.id, sys.name FROM talkgroups tg +JOIN systems sys ON tg.system_id = sys.id +WHERE (tg.system_id, tg.tgid) = ANY($1) +` + +type GetTalkgroupsByPackedIDsRow struct { + Talkgroup Talkgroup `json:"talkgroup"` + System System `json:"system"` +} + +func (q *Queries) GetTalkgroupsByPackedIDs(ctx context.Context, idtuple []TalkgroupT) ([]GetTalkgroupsByPackedIDsRow, error) { + rows, err := q.db.Query(ctx, getTalkgroupsByPackedIDs, idtuple) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetTalkgroupsByPackedIDsRow + for rows.Next() { + var i GetTalkgroupsByPackedIDsRow + if err := rows.Scan( + &i.Talkgroup.ID, + &i.Talkgroup.SystemID, + &i.Talkgroup.Tgid, + &i.Talkgroup.Name, + &i.Talkgroup.AlphaTag, + &i.Talkgroup.TgGroup, + &i.Talkgroup.Frequency, + &i.Talkgroup.Metadata, + &i.Talkgroup.Tags, + &i.Talkgroup.Alert, + &i.Talkgroup.AlertConfig, + &i.Talkgroup.Weight, + &i.System.ID, + &i.System.Name, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + + diff --git a/pkg/database/talkgroups.sql.go b/pkg/database/talkgroups.sql.go index 732b26e..1a2e37c 100644 --- a/pkg/database/talkgroups.sql.go +++ b/pkg/database/talkgroups.sql.go @@ -10,6 +10,8 @@ import ( "dynatron.me/x/stillbox/internal/jsontypes" "dynatron.me/x/stillbox/pkg/alerting/rules" + "github.com/google/uuid" + "github.com/jackc/pgx/v5/pgtype" ) const bulkSetTalkgroupTags = `-- name: BulkSetTalkgroupTags :exec @@ -17,7 +19,7 @@ UPDATE talkgroups SET tags = $2 WHERE id = ANY($1) ` -func (q *Queries) BulkSetTalkgroupTags(ctx context.Context, iD int64, tags []string) error { +func (q *Queries) BulkSetTalkgroupTags(ctx context.Context, iD uuid.UUID, tags []string) error { _, err := q.db.Exec(ctx, bulkSetTalkgroupTags, iD, tags) return err } @@ -35,14 +37,14 @@ func (q *Queries) GetSystemName(ctx context.Context, systemID int) (string, erro const getTalkgroup = `-- name: GetTalkgroup :one SELECT talkgroups.id, talkgroups.system_id, talkgroups.tgid, talkgroups.name, talkgroups.alpha_tag, talkgroups.tg_group, talkgroups.frequency, talkgroups.metadata, talkgroups.tags, talkgroups.alert, talkgroups.alert_config, talkgroups.weight FROM talkgroups -WHERE id = systg2id($1, $2) +WHERE (system_id, tgid) = ($1, $2) ` type GetTalkgroupRow struct { Talkgroup Talkgroup `json:"talkgroup"` } -func (q *Queries) GetTalkgroup(ctx context.Context, systemID int, tgid int) (GetTalkgroupRow, error) { +func (q *Queries) GetTalkgroup(ctx context.Context, systemID int32, tgid int32) (GetTalkgroupRow, error) { row := q.db.QueryRow(ctx, getTalkgroup, systemID, tgid) var i GetTalkgroupRow err := row.Scan( @@ -112,7 +114,7 @@ tg.id, tg.system_id, tg.tgid, tg.name, tg.alpha_tag, tg.tg_group, tg.frequency, FALSE learned FROM talkgroups tg JOIN systems sys ON tg.system_id = sys.id -WHERE tg.id = systg2id($1, $2) +WHERE (tg.system_id, tg.tgid) = ($1, $2) UNION SELECT tgl.id::INT8, tgl.system_id::INT4, tgl.tgid::INT4, tgl.name, @@ -131,7 +133,7 @@ type GetTalkgroupWithLearnedRow struct { Learned bool `json:"learned"` } -func (q *Queries) GetTalkgroupWithLearned(ctx context.Context, systemID int, tgid int) (GetTalkgroupWithLearnedRow, error) { +func (q *Queries) GetTalkgroupWithLearned(ctx context.Context, systemID int32, tgid int32) (GetTalkgroupWithLearnedRow, error) { row := q.db.QueryRow(ctx, getTalkgroupWithLearned, systemID, tgid) var i GetTalkgroupWithLearnedRow err := row.Scan( @@ -154,52 +156,6 @@ func (q *Queries) GetTalkgroupWithLearned(ctx context.Context, systemID int, tgi return i, err } -const getTalkgroupsByPackedIDs = `-- name: GetTalkgroupsByPackedIDs :many -SELECT tg.id, tg.system_id, tg.tgid, tg.name, tg.alpha_tag, tg.tg_group, tg.frequency, tg.metadata, tg.tags, tg.alert, tg.alert_config, tg.weight, sys.id, sys.name FROM talkgroups tg -JOIN systems sys ON tg.system_id = sys.id -WHERE tg.id = ANY($1::INT8[]) -` - -type GetTalkgroupsByPackedIDsRow struct { - Talkgroup Talkgroup `json:"talkgroup"` - System System `json:"system"` -} - -func (q *Queries) GetTalkgroupsByPackedIDs(ctx context.Context, dollar_1 []int64) ([]GetTalkgroupsByPackedIDsRow, error) { - rows, err := q.db.Query(ctx, getTalkgroupsByPackedIDs, dollar_1) - if err != nil { - return nil, err - } - defer rows.Close() - var items []GetTalkgroupsByPackedIDsRow - for rows.Next() { - var i GetTalkgroupsByPackedIDsRow - if err := rows.Scan( - &i.Talkgroup.ID, - &i.Talkgroup.SystemID, - &i.Talkgroup.Tgid, - &i.Talkgroup.Name, - &i.Talkgroup.AlphaTag, - &i.Talkgroup.TgGroup, - &i.Talkgroup.Frequency, - &i.Talkgroup.Metadata, - &i.Talkgroup.Tags, - &i.Talkgroup.Alert, - &i.Talkgroup.AlertConfig, - &i.Talkgroup.Weight, - &i.System.ID, - &i.System.Name, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - const getTalkgroupsWithAllTags = `-- name: GetTalkgroupsWithAllTags :many SELECT talkgroups.id, talkgroups.system_id, talkgroups.tgid, talkgroups.name, talkgroups.alpha_tag, talkgroups.tg_group, talkgroups.frequency, talkgroups.metadata, talkgroups.tags, talkgroups.alert, talkgroups.alert_config, talkgroups.weight FROM talkgroups WHERE tags && ARRAY[$1] @@ -292,7 +248,7 @@ FROM talkgroups tg JOIN systems sys ON tg.system_id = sys.id UNION SELECT -tgl.id::INT8, tgl.system_id::INT4, tgl.tgid::INT4, tgl.name, +NULL::UUID, tgl.system_id::INT4, tgl.tgid::INT4, tgl.name, tgl.alpha_tag, tgl.alpha_tag, NULL::INTEGER, NULL::JSONB, CASE WHEN tgl.alpha_tag IS NULL THEN NULL ELSE ARRAY[tgl.alpha_tag] END, TRUE, NULL::JSONB, 1.0, sys.id, sys.name, @@ -344,67 +300,6 @@ func (q *Queries) GetTalkgroupsWithLearned(ctx context.Context) ([]GetTalkgroups return items, nil } -const getTalkgroupsWithLearnedByPackedIDs = `-- name: GetTalkgroupsWithLearnedByPackedIDs :many -SELECT -tg.id, tg.system_id, tg.tgid, tg.name, tg.alpha_tag, tg.tg_group, tg.frequency, tg.metadata, tg.tags, tg.alert, tg.alert_config, tg.weight, sys.id, sys.name, -FALSE learned -FROM talkgroups tg -JOIN systems sys ON tg.system_id = sys.id -WHERE tg.id = ANY($1::INT8[]) -UNION -SELECT -tgl.id::INT8, tgl.system_id::INT4, tgl.tgid::INT4, tgl.name, -tgl.alpha_tag, tgl.alpha_tag, NULL::INTEGER, NULL::JSONB, -CASE WHEN tgl.alpha_tag IS NULL THEN NULL ELSE ARRAY[tgl.alpha_tag] END, -TRUE, NULL::JSONB, 1.0, sys.id, sys.name, -TRUE learned -FROM talkgroups_learned tgl -JOIN systems sys ON tgl.system_id = sys.id -WHERE systg2id(tgl.system_id, tgl.tgid) = ANY($1::INT8[]) AND ignored IS NOT TRUE -` - -type GetTalkgroupsWithLearnedByPackedIDsRow struct { - Talkgroup Talkgroup `json:"talkgroup"` - System System `json:"system"` - Learned bool `json:"learned"` -} - -func (q *Queries) GetTalkgroupsWithLearnedByPackedIDs(ctx context.Context, dollar_1 []int64) ([]GetTalkgroupsWithLearnedByPackedIDsRow, error) { - rows, err := q.db.Query(ctx, getTalkgroupsWithLearnedByPackedIDs, dollar_1) - if err != nil { - return nil, err - } - defer rows.Close() - var items []GetTalkgroupsWithLearnedByPackedIDsRow - for rows.Next() { - var i GetTalkgroupsWithLearnedByPackedIDsRow - if err := rows.Scan( - &i.Talkgroup.ID, - &i.Talkgroup.SystemID, - &i.Talkgroup.Tgid, - &i.Talkgroup.Name, - &i.Talkgroup.AlphaTag, - &i.Talkgroup.TgGroup, - &i.Talkgroup.Frequency, - &i.Talkgroup.Metadata, - &i.Talkgroup.Tags, - &i.Talkgroup.Alert, - &i.Talkgroup.AlertConfig, - &i.Talkgroup.Weight, - &i.System.ID, - &i.System.Name, - &i.Learned, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - const getTalkgroupsWithLearnedBySystem = `-- name: GetTalkgroupsWithLearnedBySystem :many SELECT tg.id, tg.system_id, tg.tgid, tg.name, tg.alpha_tag, tg.tg_group, tg.frequency, tg.metadata, tg.tags, tg.alert, tg.alert_config, tg.weight, sys.id, sys.name, @@ -414,7 +309,7 @@ JOIN systems sys ON tg.system_id = sys.id WHERE tg.system_id = $1 UNION SELECT -tgl.id::INT8, tgl.system_id::INT4, tgl.tgid::INT4, tgl.name, +NULL::UUID, tgl.system_id::INT4, tgl.tgid::INT4, tgl.name, tgl.alpha_tag, tgl.alpha_tag, NULL::INTEGER, NULL::JSONB, CASE WHEN tgl.alpha_tag IS NULL THEN NULL ELSE ARRAY[tgl.alpha_tag] END, TRUE, NULL::JSONB, 1.0, sys.id, sys.name, @@ -488,7 +383,7 @@ SET alert = COALESCE($7, alert), alert_config = COALESCE($8, alert_config), weight = COALESCE($9, weight) -WHERE id = $10 +WHERE id = $10 OR (system_id = $11 AND tgid = $12) RETURNING id, system_id, tgid, name, alpha_tag, tg_group, frequency, metadata, tags, alert, alert_config, weight ` @@ -502,7 +397,9 @@ type UpdateTalkgroupParams struct { Alert *bool `json:"alert"` AlertConfig rules.AlertRules `json:"alert_config"` Weight *float32 `json:"weight"` - ID int64 `json:"id"` + ID pgtype.UUID `json:"id"` + SystemID *int32 `json:"system_id"` + Tgid *int32 `json:"tgid"` } func (q *Queries) UpdateTalkgroup(ctx context.Context, arg UpdateTalkgroupParams) (Talkgroup, error) { @@ -517,6 +414,8 @@ func (q *Queries) UpdateTalkgroup(ctx context.Context, arg UpdateTalkgroupParams arg.AlertConfig, arg.Weight, arg.ID, + arg.SystemID, + arg.Tgid, ) var i Talkgroup err := row.Scan( diff --git a/pkg/rest/talkgroups.go b/pkg/rest/talkgroups.go index 7185148..1a539f1 100644 --- a/pkg/rest/talkgroups.go +++ b/pkg/rest/talkgroups.go @@ -97,7 +97,6 @@ func (tga *talkgroupAPI) put(w http.ResponseWriter, r *http.Request) { wErr(w, r, badRequest(err)) return } - input.ID = id.ToID().Pack() record, err := tgs.UpdateTG(ctx, input) if err != nil { diff --git a/pkg/talkgroups/cache.go b/pkg/talkgroups/cache.go index e5463c7..18f30b6 100644 --- a/pkg/talkgroups/cache.go +++ b/pkg/talkgroups/cache.go @@ -40,7 +40,7 @@ type Store interface { Hint(ctx context.Context, tgs []ID) error // Load loads the provided packed talkgroup IDs into the Store. - Load(ctx context.Context, tgs []int64) error + Load(ctx context.Context, tgs []database.TalkgroupT) error // Invalidate invalidates any caching in the Store. Invalidate() @@ -98,19 +98,19 @@ func NewCache() Store { func (t *cache) Hint(ctx context.Context, tgs []ID) error { t.RLock() - var toLoad []int64 + var toLoad []database.TalkgroupT if len(t.tgs) > len(tgs)/2 { // TODO: instrument this for _, tg := range tgs { _, ok := t.tgs[tg] if !ok { - toLoad = append(toLoad, tg.Pack()) + toLoad = append(toLoad, tg.Tuple()) } } } else { - toLoad = make([]int64, 0, len(tgs)) + toLoad = make([]database.TalkgroupT, 0, len(tgs)) for _, g := range tgs { - toLoad = append(toLoad, g.Pack()) + toLoad = append(toLoad, g.Tuple()) } } @@ -180,7 +180,7 @@ func (t *cache) TGs(ctx context.Context, tgs IDs) ([]*Talkgroup, error) { } t.RUnlock() - tgRecords, err := database.FromCtx(ctx).GetTalkgroupsWithLearnedByPackedIDs(ctx, toGet.Packed()) + tgRecords, err := database.FromCtx(ctx).GetTalkgroupsWithLearnedByPackedIDs(ctx, toGet.Tuples()) if err != nil { return nil, err } @@ -196,7 +196,7 @@ func (t *cache) TGs(ctx context.Context, tgs IDs) ([]*Talkgroup, error) { return addToRowList(t, r, tgRecords) } -func (t *cache) Load(ctx context.Context, tgs []int64) error { +func (t *cache) Load(ctx context.Context, tgs []database.TalkgroupT) error { tgRecords, err := database.FromCtx(ctx).GetTalkgroupsWithLearnedByPackedIDs(ctx, tgs) if err != nil { return err @@ -245,7 +245,7 @@ func (t *cache) TG(ctx context.Context, tg ID) (*Talkgroup, error) { return rec, nil } - recs, err := database.FromCtx(ctx).GetTalkgroupsWithLearnedByPackedIDs(ctx, []int64{tg.Pack()}) + recs, err := database.FromCtx(ctx).GetTalkgroupsWithLearnedByPackedIDs(ctx, []database.TalkgroupT{tg.Tuple()}) switch err { case nil: case pgx.ErrNoRows: @@ -290,7 +290,7 @@ func (t *cache) SystemName(ctx context.Context, id int) (name string, has bool) } func (t *cache) UpdateTG(ctx context.Context, input database.UpdateTalkgroupParams) (*Talkgroup, error) { - sysName, has := t.SystemName(ctx, int(Unpack(input.ID).System)) + sysName, has := t.SystemName(ctx, int(*input.SystemID)) if !has { return nil, ErrNoSuchSystem } diff --git a/pkg/talkgroups/import.go b/pkg/talkgroups/import.go index 05c8d2e..e58a592 100644 --- a/pkg/talkgroups/import.go +++ b/pkg/talkgroups/import.go @@ -11,6 +11,8 @@ import ( "strings" "time" + "github.com/google/uuid" + "dynatron.me/x/stillbox/internal/jsontypes" "dynatron.me/x/stillbox/pkg/database" ) @@ -116,7 +118,7 @@ func (rr *radioReferenceImporter) importTalkgroups(ctx context.Context, sys int, gn := groupName // must take a copy tgs = append(tgs, Talkgroup{ Talkgroup: database.Talkgroup{ - ID: tgt.Pack(), + ID: uuid.New(), Tgid: int32(tgt.Talkgroup), SystemID: int32(tgt.System), Name: &fields[4], diff --git a/pkg/talkgroups/talkgroup.go b/pkg/talkgroups/talkgroup.go index b9b746b..2e5b269 100644 --- a/pkg/talkgroups/talkgroup.go +++ b/pkg/talkgroups/talkgroup.go @@ -26,10 +26,10 @@ type ID struct { type IDs []ID -func (ids *IDs) Packed() []int64 { - r := make([]int64, len(*ids)) +func (ids *IDs) Tuples() []database.TalkgroupT { + r := make([]database.TalkgroupT, len(*ids)) for i := range *ids { - r[i] = (*ids)[i].Pack() + r[i] = (*ids)[i].Tuple() } return r @@ -46,15 +46,10 @@ func TG[T intId, U intId](sys T, tgid U) ID { } } -func (t ID) Pack() int64 { - // P25 system IDs are 12 bits, so we can fit them in a signed 8 byte int (int64, pg INT8) - return int64((int64(t.System) << 32) | int64(t.Talkgroup)) -} - -func Unpack(id int64) ID { - return ID{ - System: uint32(id >> 32), - Talkgroup: uint32(id & 0xffffffff), +func (t ID) Tuple() database.TalkgroupT { + return database.TalkgroupT{ + System: t.System, + Talkgroup: t.Talkgroup, } } diff --git a/sql/postgres/migrations/002_reid.down.sql b/sql/postgres/migrations/002_reid.down.sql new file mode 100644 index 0000000..b27dba3 --- /dev/null +++ b/sql/postgres/migrations/002_reid.down.sql @@ -0,0 +1,9 @@ +DROP INDEX IF EXISTS talkgroups_system_tgid_idx; + +ALTER TABLE talkgroups ALTER COLUMN id SET DATA TYPE INT8 USING (systg2id(system_id, tgid)); + +ALTER TABLE talkgroups DROP COLUMN IF EXISTS tgid; +ALTER TABLE talkgroups ADD COLUMN IF NOT EXISTS tgid INT4 NOT NULL GENERATED ALWAYS AS (id & x'ffffffff'::BIGINT) STORED, + +ALTER TABLE talkgroups DROP COLUMN IF EXISTS system_id; +ALTER TABLE talkgroups ADD COLUMN IF NOT EXISTS system_id INT4 REFERENCES systems(id) NOT NULL GENERATED ALWAYS AS (id >> 32) STORED; diff --git a/sql/postgres/migrations/002_reid.up.sql b/sql/postgres/migrations/002_reid.up.sql new file mode 100644 index 0000000..b0d91d7 --- /dev/null +++ b/sql/postgres/migrations/002_reid.up.sql @@ -0,0 +1,7 @@ +ALTER TABLE talkgroups ALTER COLUMN system_id DROP EXPRESSION; + +ALTER TABLE talkgroups ALTER COLUMN tgid DROP EXPRESSION; + +ALTER TABLE talkgroups ALTER COLUMN id SET DATA TYPE UUID USING (gen_random_uuid()); + +CREATE INDEX IF NOT EXISTS talkgroups_system_tgid_idx ON talkgroups (system_id, tgid); diff --git a/sql/postgres/migrations/flattened_initial.sql b/sql/postgres/migrations/flattened_initial.sql new file mode 100644 index 0000000..1c4397d --- /dev/null +++ b/sql/postgres/migrations/flattened_initial.sql @@ -0,0 +1,157 @@ +CREATE TABLE IF NOT EXISTS users( + id SERIAL PRIMARY KEY, + username VARCHAR (255) UNIQUE NOT NULL, + password TEXT NOT NULL, + email TEXT NOT NULL, + is_admin BOOLEAN NOT NULL, + prefs JSONB +); + +CREATE INDEX IF NOT EXISTS users_username_idx ON users(username); + +CREATE TABLE IF NOT EXISTS api_keys( + id SERIAL PRIMARY KEY, + owner INTEGER REFERENCES users(id) NOT NULL, + created_at TIMESTAMP NOT NULL, + expires TIMESTAMP, + disabled BOOLEAN, + api_key TEXT UNIQUE NOT NULL +); + +CREATE TABLE IF NOT EXISTS systems( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL +); + +CREATE OR REPLACE FUNCTION systg2id(_sys INTEGER, _tg INTEGER) RETURNS INT8 LANGUAGE plpgsql AS +$$ +BEGIN + RETURN ((_sys::BIGINT << 32) | _tg); +END +$$; + +CREATE OR REPLACE FUNCTION tgfromid(_id INT8) RETURNS INTEGER LANGUAGE plpgsql AS +$$ +BEGIN + RETURN (_id & x'ffffffff'::BIGINT); +END +$$; + +CREATE OR REPLACE FUNCTION sysfromid(_id INT8) RETURNS INTEGER LANGUAGE plpgsql AS +$$ +BEGIN + RETURN (_id >> 32); +END +$$; + +CREATE TABLE IF NOT EXISTS talkgroups( + id UUID PRIMARY KEY, + system_id INT4 REFERENCES systems(id) NOT NULL, + tgid INT4 NOT NULL, + name TEXT, + alpha_tag TEXT, + tg_group TEXT, + frequency INTEGER, + metadata JSONB, + tags TEXT[] NOT NULL DEFAULT '{}', + alert BOOLEAN NOT NULL DEFAULT 'true', + alert_config JSONB, + weight REAL NOT NULL DEFAULT 1.0 +); + +CREATE INDEX talkgroups_system_tgid_idx ON talkgroups (system_id, tgid); + +CREATE INDEX IF NOT EXISTS talkgroup_id_tags ON talkgroups USING GIN (tags); + +CREATE TABLE IF NOT EXISTS talkgroups_learned( + id SERIAL PRIMARY KEY, + system_id INTEGER REFERENCES systems(id) NOT NULL, + tgid INTEGER NOT NULL, + name TEXT NOT NULL, + alpha_tag TEXT, + ignored BOOLEAN, + UNIQUE (system_id, tgid, name) +); + +CREATE TABLE IF NOT EXISTS alerts( + id UUID PRIMARY KEY, + time TIMESTAMPTZ NOT NULL, + tgid INTEGER NOT NULL, + system_id INTEGER REFERENCES systems(id) NOT NULL, + weight REAL, + score REAL, + orig_score REAL, + notified BOOLEAN NOT NULL DEFAULT 'false', + metadata JSONB +); + +CREATE OR REPLACE FUNCTION learn_talkgroup() +RETURNS TRIGGER AS $$ +BEGIN + IF NOT EXISTS ( + SELECT tg.system_id, tg.tgid, tg.name, tg.alpha_tag FROM talkgroups tg WHERE tg.system_id = NEW.system AND tg.tgid = NEW.talkgroup + UNION + SELECT tgl.system_id, tgl.tgid, tgl.name, tgl.alpha_tag FROM talkgroups_learned tgl WHERE tgl.system_id = NEW.system AND tgl.tgid = NEW.talkgroup + ) THEN + INSERT INTO talkgroups_learned(system_id, tgid, name, alpha_tag) VALUES( + NEW.system, NEW.talkgroup, NEW.tg_label, NEW.tg_alpha_tag + ) ON CONFLICT DO NOTHING; + END IF; + RETURN NEW; +END +$$ LANGUAGE plpgsql; + +CREATE TABLE IF NOT EXISTS calls( + id UUID PRIMARY KEY, + submitter INTEGER REFERENCES api_keys(id) ON DELETE SET NULL, + system INTEGER NOT NULL, + talkgroup INTEGER NOT NULL, + call_date TIMESTAMPTZ NOT NULL, + audio_name TEXT, + audio_blob BYTEA, + duration INTEGER, + audio_type TEXT, + audio_url TEXT, + frequency INTEGER NOT NULL, + frequencies INTEGER[], + patches INTEGER[], + tg_label TEXT, + tg_alpha_tag TEXT, + tg_group TEXT, + source INTEGER NOT NULL, + transcript TEXT +); + +CREATE OR REPLACE TRIGGER learn_tg AFTER INSERT ON calls +FOR EACH ROW EXECUTE FUNCTION learn_talkgroup(); + +CREATE INDEX IF NOT EXISTS calls_transcript_idx ON calls USING GIN (to_tsvector('english', transcript)); +CREATE INDEX IF NOT EXISTS calls_call_date_tg_idx ON calls(system, talkgroup, call_date); + +CREATE TABLE IF NOT EXISTS settings( + name TEXT PRIMARY KEY, + updated_by INTEGER REFERENCES users(id), + value JSONB +); + +CREATE TABLE IF NOT EXISTS incidents( + id UUID PRIMARY KEY, + name TEXT NOT NULL, + description TEXT, + start_time TIMESTAMP, + end_time TIMESTAMP, + location JSONB, + metadata JSONB +); + +CREATE INDEX IF NOT EXISTS incidents_name_description_idx ON incidents USING GIN ( + (to_tsvector('english', name) || to_tsvector('english', coalesce(description, '')) + ) +); + +CREATE TABLE IF NOT EXISTS incidents_calls( + incident_id UUID REFERENCES incidents(id) ON UPDATE CASCADE ON DELETE CASCADE, + call_id UUID REFERENCES calls(id) ON UPDATE CASCADE, + notes JSONB, + PRIMARY KEY (incident_id, call_id) +); diff --git a/sql/postgres/queries/talkgroups.sql b/sql/postgres/queries/talkgroups.sql index 732a848..f2e7a1e 100644 --- a/sql/postgres/queries/talkgroups.sql +++ b/sql/postgres/queries/talkgroups.sql @@ -26,12 +26,7 @@ WHERE id = ANY($1); -- name: GetTalkgroup :one SELECT sqlc.embed(talkgroups) FROM talkgroups -WHERE id = systg2id(sqlc.arg(system_id), sqlc.arg(tgid)); - --- name: GetTalkgroupsByPackedIDs :many -SELECT sqlc.embed(tg), sqlc.embed(sys) FROM talkgroups tg -JOIN systems sys ON tg.system_id = sys.id -WHERE tg.id = ANY($1::INT8[]); +WHERE (system_id, tgid) = (@system_id, @tgid); -- name: GetTalkgroupWithLearned :one SELECT @@ -39,7 +34,7 @@ sqlc.embed(tg), sqlc.embed(sys), FALSE learned FROM talkgroups tg JOIN systems sys ON tg.system_id = sys.id -WHERE tg.id = systg2id(sqlc.arg(system_id), sqlc.arg(tgid)) +WHERE (tg.system_id, tg.tgid) = (sqlc.arg(system_id), sqlc.arg(tgid)) UNION SELECT tgl.id::INT8, tgl.system_id::INT4, tgl.tgid::INT4, tgl.name, @@ -51,24 +46,6 @@ FROM talkgroups_learned tgl JOIN systems sys ON tgl.system_id = sys.id WHERE tgl.system_id = sqlc.arg(system_id) AND tgl.tgid = sqlc.arg(tgid) AND ignored IS NOT TRUE; --- name: GetTalkgroupsWithLearnedByPackedIDs :many -SELECT -sqlc.embed(tg), sqlc.embed(sys), -FALSE learned -FROM talkgroups tg -JOIN systems sys ON tg.system_id = sys.id -WHERE tg.id = ANY($1::INT8[]) -UNION -SELECT -tgl.id::INT8, tgl.system_id::INT4, tgl.tgid::INT4, tgl.name, -tgl.alpha_tag, tgl.alpha_tag, NULL::INTEGER, NULL::JSONB, -CASE WHEN tgl.alpha_tag IS NULL THEN NULL ELSE ARRAY[tgl.alpha_tag] END, -TRUE, NULL::JSONB, 1.0, sys.id, sys.name, -TRUE learned -FROM talkgroups_learned tgl -JOIN systems sys ON tgl.system_id = sys.id -WHERE systg2id(tgl.system_id, tgl.tgid) = ANY($1::INT8[]) AND ignored IS NOT TRUE; - -- name: GetTalkgroupsWithLearnedBySystem :many SELECT sqlc.embed(tg), sqlc.embed(sys), @@ -78,7 +55,7 @@ JOIN systems sys ON tg.system_id = sys.id WHERE tg.system_id = @system UNION SELECT -tgl.id::INT8, tgl.system_id::INT4, tgl.tgid::INT4, tgl.name, +NULL::UUID, tgl.system_id::INT4, tgl.tgid::INT4, tgl.name, tgl.alpha_tag, tgl.alpha_tag, NULL::INTEGER, NULL::JSONB, CASE WHEN tgl.alpha_tag IS NULL THEN NULL ELSE ARRAY[tgl.alpha_tag] END, TRUE, NULL::JSONB, 1.0, sys.id, sys.name, @@ -95,7 +72,7 @@ FROM talkgroups tg JOIN systems sys ON tg.system_id = sys.id UNION SELECT -tgl.id::INT8, tgl.system_id::INT4, tgl.tgid::INT4, tgl.name, +NULL::UUID, tgl.system_id::INT4, tgl.tgid::INT4, tgl.name, tgl.alpha_tag, tgl.alpha_tag, NULL::INTEGER, NULL::JSONB, CASE WHEN tgl.alpha_tag IS NULL THEN NULL ELSE ARRAY[tgl.alpha_tag] END, TRUE, NULL::JSONB, 1.0, sys.id, sys.name, @@ -119,5 +96,5 @@ SET alert = COALESCE(sqlc.narg('alert'), alert), alert_config = COALESCE(sqlc.narg('alert_config'), alert_config), weight = COALESCE(sqlc.narg('weight'), weight) -WHERE id = @id +WHERE id = sqlc.narg('id') OR (system_id = sqlc.narg('system_id') AND tgid = sqlc.narg('tgid')) RETURNING *; diff --git a/sql/sqlc.yaml b/sql/sqlc.yaml index 342e2fb..55a0959 100644 --- a/sql/sqlc.yaml +++ b/sql/sqlc.yaml @@ -37,3 +37,7 @@ sql: import: "dynatron.me/x/stillbox/internal/jsontypes" type: "Metadata" nullable: true + - column: "(talkgroups.system_id, talkgroups.tgid)" + go_type: + import: "dynatron.me/x/stillbox/pkg/talkgroups" + type: "ID" -- 2.47.0 From 05eccf588ba87f3b48c422cb8fad5f3b9b951bfe Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Fri, 15 Nov 2024 08:46:29 -0500 Subject: [PATCH 08/17] wip --- .gitignore | 2 +- pkg/alerting/stats.go | 4 +- pkg/database/database.go | 14 +++++++ pkg/database/extend.go | 6 +-- pkg/database/talkgroups.manual.sql.go | 59 +++++++++++++++++---------- pkg/talkgroups/cache.go | 2 +- pkg/talkgroups/talkgroup.go | 2 +- 7 files changed, 59 insertions(+), 30 deletions(-) diff --git a/.gitignore b/.gitignore index cb1321f..8e9a60e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ config.yaml config.test.yaml -mydb.sql +/*.sql client/calls/ !client/calls/.gitkeep /gordio diff --git a/pkg/alerting/stats.go b/pkg/alerting/stats.go index 6ba16dd..5dea62e 100644 --- a/pkg/alerting/stats.go +++ b/pkg/alerting/stats.go @@ -47,13 +47,13 @@ func (as *alerter) tgStatsHandler(w http.ResponseWriter, r *http.Request) { return } - tgMap := make(map[talkgroups.ID]database.GetTalkgroupsWithLearnedByPackedIDsRow, len(tgs)) + tgMap := make(map[talkgroups.ID]database.GetTalkgroupsRow, len(tgs)) for _, t := range tgs { tgMap[talkgroups.ID{System: uint32(t.System.ID), Talkgroup: uint32(t.Talkgroup.Tgid)}] = t } renderData := struct { - TGs map[talkgroups.ID]database.GetTalkgroupsWithLearnedByPackedIDsRow + TGs map[talkgroups.ID]database.GetTalkgroupsRow Scores trending.Scores[talkgroups.ID] LastScore time.Time Simulation *Simulation diff --git a/pkg/database/database.go b/pkg/database/database.go index 9f1c69b..ebc5a6f 100644 --- a/pkg/database/database.go +++ b/pkg/database/database.go @@ -11,6 +11,8 @@ import ( _ "github.com/golang-migrate/migrate/v4/database/pgx/v5" "github.com/golang-migrate/migrate/v4/source/iofs" "github.com/jackc/pgx/v5/pgxpool" + "github.com/jackc/pgx/v5/tracelog" + "github.com/rs/zerolog/log" ) // DB is a database handle. @@ -19,6 +21,12 @@ type DB struct { *Queries } +type myLogger struct{} + +func (m myLogger) Log(ctx context.Context, level tracelog.LogLevel, msg string, data map[string]any) { + log.Debug().Fields(data).Msg(msg) +} + // NewClient creates a new DB using the provided config. func NewClient(ctx context.Context, conf config.DB) (*DB, error) { dir, err := iofs.New(sqlembed.Migrations, "postgres/migrations") @@ -43,6 +51,12 @@ func NewClient(ctx context.Context, conf config.DB) (*DB, error) { return nil, err } + logger := myLogger{} + tracer := &tracelog.TraceLog{ + Logger: logger, + LogLevel: tracelog.LogLevelTrace, + } + pgConf.ConnConfig.Tracer = tracer pool, err := pgxpool.NewWithConfig(ctx, pgConf) if err != nil { return nil, err diff --git a/pkg/database/extend.go b/pkg/database/extend.go index 5f165a0..61619d4 100644 --- a/pkg/database/extend.go +++ b/pkg/database/extend.go @@ -1,8 +1,8 @@ package database -func (d GetTalkgroupsWithLearnedByPackedIDsRow) GetTalkgroup() Talkgroup { return d.Talkgroup } -func (d GetTalkgroupsWithLearnedByPackedIDsRow) GetSystem() System { return d.System } -func (d GetTalkgroupsWithLearnedByPackedIDsRow) GetLearned() bool { return d.Learned } +func (d GetTalkgroupsRow) GetTalkgroup() Talkgroup { return d.Talkgroup } +func (d GetTalkgroupsRow) GetSystem() System { return d.System } +func (d GetTalkgroupsRow) GetLearned() bool { return d.Learned } func (g GetTalkgroupsWithLearnedRow) GetTalkgroup() Talkgroup { return g.Talkgroup } func (g GetTalkgroupsWithLearnedRow) GetSystem() System { return g.System } func (g GetTalkgroupsWithLearnedRow) GetLearned() bool { return g.Learned } diff --git a/pkg/database/talkgroups.manual.sql.go b/pkg/database/talkgroups.manual.sql.go index c141e23..debebe5 100644 --- a/pkg/database/talkgroups.manual.sql.go +++ b/pkg/database/talkgroups.manual.sql.go @@ -3,24 +3,45 @@ package database import ( "context" "database/sql/driver" + "fmt" + + "github.com/jackc/pgx/v5/pgtype" ) type TalkgroupT struct { - System uint32 `json:"sys"` - Talkgroup uint32 `json:"tg"` + System uint32 `json:"system_id"` + Talkgroup uint32 `json:"tgid"` +} + +type TalkgroupTs []TalkgroupT + +func (t TalkgroupTs) Nest() (sys []uint32, tg []uint32) { + sys = make([]uint32, len(t)) + tg = make([]uint32, len(t)) + + for i := range t { + sys[i] = t[i].System + tg[i] = t[i].Talkgroup + } + + return } func (t TalkgroupT) Value() (driver.Value, error) { return [2]uint32{t.System, t.Talkgroup}, nil } +func (t TalkgroupT) TextValue() (pgtype.Text, error) { + return pgtype.Text{String: fmt.Sprintf("%d:%d", t.System, t.Talkgroup)}, nil +} + const getTalkgroupsWithLearnedByPackedIDs = `-- name: GetTalkgroupsWithLearnedByPackedIDs :many SELECT tg.id, tg.system_id, tg.tgid, tg.name, tg.alpha_tag, tg.tg_group, tg.frequency, tg.metadata, tg.tags, tg.alert, tg.alert_config, tg.weight, sys.id, sys.name, FALSE learned FROM talkgroups tg JOIN systems sys ON tg.system_id = sys.id -WHERE (tg.system_id, tg.tgid) = ANY($1) +JOIN UNNEST($1::INT4[], $2::INT4[]) AS tgt(sys, tg) ON (tg.system_id = tgt.sys AND tg.tgid = tgt.tg) UNION SELECT NULL::UUID, tgl.system_id::INT4, tgl.tgid::INT4, tgl.name, @@ -30,24 +51,24 @@ TRUE, NULL::JSONB, 1.0, sys.id, sys.name, TRUE learned FROM talkgroups_learned tgl JOIN systems sys ON tgl.system_id = sys.id -WHERE (tgl.system_id, tgl.tgid) = ANY($1); -` +JOIN UNNEST($1::INT4[], $2::INT4[]) AS tgt(sys, tg) ON (tgl.system_id = tgt.sys AND tgl.tgid = tgt.tg);` -type GetTalkgroupsWithLearnedByPackedIDsRow struct { +type GetTalkgroupsRow struct { Talkgroup Talkgroup `json:"talkgroup"` System System `json:"system"` Learned bool `json:"learned"` } -func (q *Queries) GetTalkgroupsWithLearnedByPackedIDs(ctx context.Context, ids []TalkgroupT) ([]GetTalkgroupsWithLearnedByPackedIDsRow, error) { - rows, err := q.db.Query(ctx, getTalkgroupsWithLearnedByPackedIDs, ids) +func (q *Queries) GetTalkgroupsWithLearnedByPackedIDs(ctx context.Context, ids TalkgroupTs) ([]GetTalkgroupsRow, error) { + sysAr, tgAr := ids.Nest() + rows, err := q.db.Query(ctx, getTalkgroupsWithLearnedByPackedIDs, sysAr, tgAr) if err != nil { return nil, err } defer rows.Close() - var items []GetTalkgroupsWithLearnedByPackedIDsRow + var items []GetTalkgroupsRow for rows.Next() { - var i GetTalkgroupsWithLearnedByPackedIDsRow + var i GetTalkgroupsRow if err := rows.Scan( &i.Talkgroup.ID, &i.Talkgroup.SystemID, @@ -78,23 +99,19 @@ func (q *Queries) GetTalkgroupsWithLearnedByPackedIDs(ctx context.Context, ids [ const getTalkgroupsByPackedIDs = `-- name: GetTalkgroupsByPackedIDs :many SELECT tg.id, tg.system_id, tg.tgid, tg.name, tg.alpha_tag, tg.tg_group, tg.frequency, tg.metadata, tg.tags, tg.alert, tg.alert_config, tg.weight, sys.id, sys.name FROM talkgroups tg JOIN systems sys ON tg.system_id = sys.id -WHERE (tg.system_id, tg.tgid) = ANY($1) +JOIN UNNEST($1::INT4[], $2::INT4[]) AS tgt(sys, tg) ON (tg.system_id = tgt.sys AND tg.tgid = tgt.tg) ` -type GetTalkgroupsByPackedIDsRow struct { - Talkgroup Talkgroup `json:"talkgroup"` - System System `json:"system"` -} - -func (q *Queries) GetTalkgroupsByPackedIDs(ctx context.Context, idtuple []TalkgroupT) ([]GetTalkgroupsByPackedIDsRow, error) { - rows, err := q.db.Query(ctx, getTalkgroupsByPackedIDs, idtuple) +func (q *Queries) GetTalkgroupsByPackedIDs(ctx context.Context, ids TalkgroupTs) ([]GetTalkgroupsRow, error) { + sysAr, tgAr := ids.Nest() + rows, err := q.db.Query(ctx, getTalkgroupsByPackedIDs, sysAr, tgAr) if err != nil { return nil, err } defer rows.Close() - var items []GetTalkgroupsByPackedIDsRow + var items []GetTalkgroupsRow for rows.Next() { - var i GetTalkgroupsByPackedIDsRow + var i GetTalkgroupsRow if err := rows.Scan( &i.Talkgroup.ID, &i.Talkgroup.SystemID, @@ -120,5 +137,3 @@ func (q *Queries) GetTalkgroupsByPackedIDs(ctx context.Context, idtuple []Talkgr } return items, nil } - - diff --git a/pkg/talkgroups/cache.go b/pkg/talkgroups/cache.go index 18f30b6..0e86b2c 100644 --- a/pkg/talkgroups/cache.go +++ b/pkg/talkgroups/cache.go @@ -135,7 +135,7 @@ func (t *cache) add(rec *Talkgroup) error { } type row interface { - database.GetTalkgroupsWithLearnedByPackedIDsRow | database.GetTalkgroupsWithLearnedRow | + database.GetTalkgroupsRow | database.GetTalkgroupsWithLearnedRow | database.GetTalkgroupsWithLearnedBySystemRow GetTalkgroup() database.Talkgroup GetSystem() database.System diff --git a/pkg/talkgroups/talkgroup.go b/pkg/talkgroups/talkgroup.go index 2e5b269..5849ebb 100644 --- a/pkg/talkgroups/talkgroup.go +++ b/pkg/talkgroups/talkgroup.go @@ -48,7 +48,7 @@ func TG[T intId, U intId](sys T, tgid U) ID { func (t ID) Tuple() database.TalkgroupT { return database.TalkgroupT{ - System: t.System, + System: t.System, Talkgroup: t.Talkgroup, } } -- 2.47.0 From af80e46068825856171936da8f2892793a6e1f71 Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Fri, 15 Nov 2024 10:37:58 -0500 Subject: [PATCH 09/17] New DB schema --- pkg/alerting/alerting.go | 8 +- pkg/alerting/stats.go | 2 +- pkg/config/config.go | 3 +- pkg/database/database.go | 15 +- pkg/database/extend.go | 27 +-- pkg/database/querier.go | 7 +- ...talkgroups.manual.sql.go => talkgroups.go} | 59 +++---- pkg/database/talkgroups.sql.go | 31 ++-- pkg/database/talkgroups.sql_test.go | 27 +-- pkg/talkgroups/cache.go | 33 ++-- pkg/talkgroups/talkgroup.go | 20 +-- sql/postgres/migrations/001_initial.up.sql | 32 +--- sql/postgres/migrations/002_reid.down.sql | 9 - sql/postgres/migrations/002_reid.up.sql | 7 - sql/postgres/migrations/flattened_initial.sql | 157 ------------------ sql/postgres/queries/talkgroups.sql | 26 ++- 16 files changed, 111 insertions(+), 352 deletions(-) rename pkg/database/{talkgroups.manual.sql.go => talkgroups.go} (61%) delete mode 100644 sql/postgres/migrations/002_reid.down.sql delete mode 100644 sql/postgres/migrations/002_reid.up.sql delete mode 100644 sql/postgres/migrations/flattened_initial.sql diff --git a/pkg/alerting/alerting.go b/pkg/alerting/alerting.go index ee3ec5f..316f266 100644 --- a/pkg/alerting/alerting.go +++ b/pkg/alerting/alerting.go @@ -228,11 +228,11 @@ func (as *alerter) scoredTGs() []talkgroups.ID { return tgs } -// packedScoredTGs gets a list of packed TGIDs. -func (as *alerter) scoredTGsTuple() []database.TalkgroupT { - tgs := make([]database.TalkgroupT, 0, len(as.scores)) +// packedScoredTGs gets a list of TGID tuples. +func (as *alerter) scoredTGsTuple() (tgs database.TGTuples) { + tgs = database.MakeTGTuples(len(as.scores)) for _, s := range as.scores { - tgs = append(tgs, s.ID.Tuple()) + tgs.Append(s.ID.System, s.ID.Talkgroup) } return tgs diff --git a/pkg/alerting/stats.go b/pkg/alerting/stats.go index 5dea62e..3ace635 100644 --- a/pkg/alerting/stats.go +++ b/pkg/alerting/stats.go @@ -40,7 +40,7 @@ func (as *alerter) tgStatsHandler(w http.ResponseWriter, r *http.Request) { ctx := r.Context() db := database.FromCtx(ctx) - tgs, err := db.GetTalkgroupsWithLearnedByPackedIDs(ctx, as.scoredTGsTuple()) + tgs, err := db.GetTalkgroupsWithLearnedBySysTGID(ctx, as.scoredTGsTuple()) if err != nil { log.Error().Err(err).Msg("stats TG get failed") http.Error(w, err.Error(), http.StatusInternalServerError) diff --git a/pkg/config/config.go b/pkg/config/config.go index 47e288a..fb2c589 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -37,7 +37,8 @@ type CORS struct { } type DB struct { - Connect string `yaml:"connect"` + Connect string `yaml:"connect"` + LogQueries bool `yaml:"logQueries"` } type Logger struct { diff --git a/pkg/database/database.go b/pkg/database/database.go index ebc5a6f..d1a7e06 100644 --- a/pkg/database/database.go +++ b/pkg/database/database.go @@ -21,9 +21,9 @@ type DB struct { *Queries } -type myLogger struct{} +type dbLogger struct{} -func (m myLogger) Log(ctx context.Context, level tracelog.LogLevel, msg string, data map[string]any) { +func (m dbLogger) Log(ctx context.Context, level tracelog.LogLevel, msg string, data map[string]any) { log.Debug().Fields(data).Msg(msg) } @@ -51,12 +51,13 @@ func NewClient(ctx context.Context, conf config.DB) (*DB, error) { return nil, err } - logger := myLogger{} - tracer := &tracelog.TraceLog{ - Logger: logger, - LogLevel: tracelog.LogLevelTrace, + if conf.LogQueries { + pgConf.ConnConfig.Tracer = &tracelog.TraceLog{ + Logger: dbLogger{}, + LogLevel: tracelog.LogLevelTrace, + } } - pgConf.ConnConfig.Tracer = tracer + pool, err := pgxpool.NewWithConfig(ctx, pgConf) if err != nil { return nil, err diff --git a/pkg/database/extend.go b/pkg/database/extend.go index 61619d4..d885991 100644 --- a/pkg/database/extend.go +++ b/pkg/database/extend.go @@ -1,14 +1,17 @@ package database -func (d GetTalkgroupsRow) GetTalkgroup() Talkgroup { return d.Talkgroup } -func (d GetTalkgroupsRow) GetSystem() System { return d.System } -func (d GetTalkgroupsRow) GetLearned() bool { return d.Learned } -func (g GetTalkgroupsWithLearnedRow) GetTalkgroup() Talkgroup { return g.Talkgroup } -func (g GetTalkgroupsWithLearnedRow) GetSystem() System { return g.System } -func (g GetTalkgroupsWithLearnedRow) GetLearned() bool { return g.Learned } -func (g GetTalkgroupsWithLearnedBySystemRow) GetTalkgroup() Talkgroup { return g.Talkgroup } -func (g GetTalkgroupsWithLearnedBySystemRow) GetSystem() System { return g.System } -func (g GetTalkgroupsWithLearnedBySystemRow) GetLearned() bool { return g.Learned } -func (g Talkgroup) GetTalkgroup() Talkgroup { return g } -func (g Talkgroup) GetSystem() System { return System{ID: int(g.SystemID)} } -func (g Talkgroup) GetLearned() bool { return false } +func (d GetTalkgroupsRow) GetTalkgroup() Talkgroup { return d.Talkgroup } +func (d GetTalkgroupsRow) GetSystem() System { return d.System } +func (d GetTalkgroupsRow) GetLearned() bool { return d.Learned } +func (g GetTalkgroupWithLearnedRow) GetTalkgroup() Talkgroup { return g.Talkgroup } +func (g GetTalkgroupWithLearnedRow) GetSystem() System { return g.System } +func (g GetTalkgroupWithLearnedRow) GetLearned() bool { return g.Learned } +func (g GetTalkgroupsWithLearnedRow) GetTalkgroup() Talkgroup { return g.Talkgroup } +func (g GetTalkgroupsWithLearnedRow) GetSystem() System { return g.System } +func (g GetTalkgroupsWithLearnedRow) GetLearned() bool { return g.Learned } +func (g GetTalkgroupsWithLearnedBySystemRow) GetTalkgroup() Talkgroup { return g.Talkgroup } +func (g GetTalkgroupsWithLearnedBySystemRow) GetSystem() System { return g.System } +func (g GetTalkgroupsWithLearnedBySystemRow) GetLearned() bool { return g.Learned } +func (g Talkgroup) GetTalkgroup() Talkgroup { return g } +func (g Talkgroup) GetSystem() System { return System{ID: int(g.SystemID)} } +func (g Talkgroup) GetLearned() bool { return false } diff --git a/pkg/database/querier.go b/pkg/database/querier.go index 8d91e22..0f72f67 100644 --- a/pkg/database/querier.go +++ b/pkg/database/querier.go @@ -14,7 +14,6 @@ import ( type Querier interface { AddAlert(ctx context.Context, arg AddAlertParams) error AddCall(ctx context.Context, arg AddCallParams) error - BulkSetTalkgroupTags(ctx context.Context, iD uuid.UUID, tags []string) error CreateAPIKey(ctx context.Context, owner int, expires pgtype.Timestamp, disabled *bool) (ApiKey, error) CreateUser(ctx context.Context, arg CreateUserParams) (User, error) DeleteAPIKey(ctx context.Context, apiKey string) error @@ -22,9 +21,9 @@ type Querier interface { GetAPIKey(ctx context.Context, apiKey string) (ApiKey, error) GetDatabaseSize(ctx context.Context) (string, error) GetSystemName(ctx context.Context, systemID int) (string, error) - GetTalkgroup(ctx context.Context, systemID int32, tgid int32) (GetTalkgroupRow, error) + GetTalkgroup(ctx context.Context, systemID int32, tgID int32) (GetTalkgroupRow, error) GetTalkgroupIDsByTags(ctx context.Context, anytags []string, alltags []string, nottags []string) ([]GetTalkgroupIDsByTagsRow, error) - GetTalkgroupTags(ctx context.Context, sys int, tg int) ([]string, error) + GetTalkgroupTags(ctx context.Context, systemID int32, tgID int32) ([]string, error) GetTalkgroupWithLearned(ctx context.Context, systemID int32, tgid int32) (GetTalkgroupWithLearnedRow, error) GetTalkgroupsWithAllTags(ctx context.Context, tags []string) ([]GetTalkgroupsWithAllTagsRow, error) GetTalkgroupsWithAnyTags(ctx context.Context, tags []string) ([]GetTalkgroupsWithAnyTagsRow, error) @@ -35,7 +34,7 @@ type Querier interface { GetUserByUsername(ctx context.Context, username string) (User, error) GetUsers(ctx context.Context) ([]User, error) SetCallTranscript(ctx context.Context, iD uuid.UUID, transcript *string) error - SetTalkgroupTags(ctx context.Context, sys int, tg int, tags []string) error + SetTalkgroupTags(ctx context.Context, tags []string, systemID int32, tgID int32) error UpdatePassword(ctx context.Context, username string, password string) error UpdateTalkgroup(ctx context.Context, arg UpdateTalkgroupParams) (Talkgroup, error) } diff --git a/pkg/database/talkgroups.manual.sql.go b/pkg/database/talkgroups.go similarity index 61% rename from pkg/database/talkgroups.manual.sql.go rename to pkg/database/talkgroups.go index debebe5..0870892 100644 --- a/pkg/database/talkgroups.manual.sql.go +++ b/pkg/database/talkgroups.go @@ -2,41 +2,25 @@ package database import ( "context" - "database/sql/driver" - "fmt" - - "github.com/jackc/pgx/v5/pgtype" ) -type TalkgroupT struct { - System uint32 `json:"system_id"` - Talkgroup uint32 `json:"tgid"` -} +type TGTuples [2][]uint32 -type TalkgroupTs []TalkgroupT - -func (t TalkgroupTs) Nest() (sys []uint32, tg []uint32) { - sys = make([]uint32, len(t)) - tg = make([]uint32, len(t)) - - for i := range t { - sys[i] = t[i].System - tg[i] = t[i].Talkgroup +func MakeTGTuples(cap int) TGTuples { + return [2][]uint32{ + make([]uint32, 0, cap), + make([]uint32, 0, cap), } - - return } -func (t TalkgroupT) Value() (driver.Value, error) { - return [2]uint32{t.System, t.Talkgroup}, nil +func (t *TGTuples) Append(sys, tg uint32) { + t[0] = append(t[0], sys) + t[1] = append(t[1], tg) } -func (t TalkgroupT) TextValue() (pgtype.Text, error) { - return pgtype.Text{String: fmt.Sprintf("%d:%d", t.System, t.Talkgroup)}, nil -} +// Below queries are here because sqlc refuses to parse unnest(x, y) -const getTalkgroupsWithLearnedByPackedIDs = `-- name: GetTalkgroupsWithLearnedByPackedIDs :many -SELECT +const getTalkgroupsWithLearnedBySysTGID = `SELECT tg.id, tg.system_id, tg.tgid, tg.name, tg.alpha_tag, tg.tg_group, tg.frequency, tg.metadata, tg.tags, tg.alert, tg.alert_config, tg.weight, sys.id, sys.name, FALSE learned FROM talkgroups tg @@ -59,9 +43,8 @@ type GetTalkgroupsRow struct { Learned bool `json:"learned"` } -func (q *Queries) GetTalkgroupsWithLearnedByPackedIDs(ctx context.Context, ids TalkgroupTs) ([]GetTalkgroupsRow, error) { - sysAr, tgAr := ids.Nest() - rows, err := q.db.Query(ctx, getTalkgroupsWithLearnedByPackedIDs, sysAr, tgAr) +func (q *Queries) GetTalkgroupsWithLearnedBySysTGID(ctx context.Context, ids TGTuples) ([]GetTalkgroupsRow, error) { + rows, err := q.db.Query(ctx, getTalkgroupsWithLearnedBySysTGID, ids[0], ids[1]) if err != nil { return nil, err } @@ -96,15 +79,12 @@ func (q *Queries) GetTalkgroupsWithLearnedByPackedIDs(ctx context.Context, ids T return items, nil } -const getTalkgroupsByPackedIDs = `-- name: GetTalkgroupsByPackedIDs :many -SELECT tg.id, tg.system_id, tg.tgid, tg.name, tg.alpha_tag, tg.tg_group, tg.frequency, tg.metadata, tg.tags, tg.alert, tg.alert_config, tg.weight, sys.id, sys.name FROM talkgroups tg +const getTalkgroupsBySysTGID = `SELECT tg.id, tg.system_id, tg.tgid, tg.name, tg.alpha_tag, tg.tg_group, tg.frequency, tg.metadata, tg.tags, tg.alert, tg.alert_config, tg.weight, sys.id, sys.name FROM talkgroups tg JOIN systems sys ON tg.system_id = sys.id -JOIN UNNEST($1::INT4[], $2::INT4[]) AS tgt(sys, tg) ON (tg.system_id = tgt.sys AND tg.tgid = tgt.tg) -` +JOIN UNNEST($1::INT4[], $2::INT4[]) AS tgt(sys, tg) ON (tg.system_id = tgt.sys AND tg.tgid = tgt.tg);` -func (q *Queries) GetTalkgroupsByPackedIDs(ctx context.Context, ids TalkgroupTs) ([]GetTalkgroupsRow, error) { - sysAr, tgAr := ids.Nest() - rows, err := q.db.Query(ctx, getTalkgroupsByPackedIDs, sysAr, tgAr) +func (q *Queries) GetTalkgroupsBySysTGID(ctx context.Context, ids TGTuples) ([]GetTalkgroupsRow, error) { + rows, err := q.db.Query(ctx, getTalkgroupsBySysTGID, ids[0], ids[1]) if err != nil { return nil, err } @@ -137,3 +117,10 @@ func (q *Queries) GetTalkgroupsByPackedIDs(ctx context.Context, ids TalkgroupTs) } return items, nil } + +const bulkSetTalkgroupTags = `UPDATE talkgroups tg SET tags = $3 FROM UNNEST($1::INT4[], $2::INT4[]) AS tgt(sys, tg) WHERE (tg.system_id = tgt.sys AND tg.tgid = tgt.tg);` + +func (q *Queries) BulkSetTalkgroupTags(ctx context.Context, tgs TGTuples, tags []string) error { + _, err := q.db.Exec(ctx, bulkSetTalkgroupTags, tgs[0], tgs[1], tags) + return err +} diff --git a/pkg/database/talkgroups.sql.go b/pkg/database/talkgroups.sql.go index 1a2e37c..a3836f9 100644 --- a/pkg/database/talkgroups.sql.go +++ b/pkg/database/talkgroups.sql.go @@ -10,20 +10,9 @@ import ( "dynatron.me/x/stillbox/internal/jsontypes" "dynatron.me/x/stillbox/pkg/alerting/rules" - "github.com/google/uuid" "github.com/jackc/pgx/v5/pgtype" ) -const bulkSetTalkgroupTags = `-- name: BulkSetTalkgroupTags :exec -UPDATE talkgroups SET tags = $2 -WHERE id = ANY($1) -` - -func (q *Queries) BulkSetTalkgroupTags(ctx context.Context, iD uuid.UUID, tags []string) error { - _, err := q.db.Exec(ctx, bulkSetTalkgroupTags, iD, tags) - return err -} - const getSystemName = `-- name: GetSystemName :one SELECT name FROM systems WHERE id = $1 ` @@ -44,8 +33,8 @@ type GetTalkgroupRow struct { Talkgroup Talkgroup `json:"talkgroup"` } -func (q *Queries) GetTalkgroup(ctx context.Context, systemID int32, tgid int32) (GetTalkgroupRow, error) { - row := q.db.QueryRow(ctx, getTalkgroup, systemID, tgid) +func (q *Queries) GetTalkgroup(ctx context.Context, systemID int32, tgID int32) (GetTalkgroupRow, error) { + row := q.db.QueryRow(ctx, getTalkgroup, systemID, tgID) var i GetTalkgroupRow err := row.Scan( &i.Talkgroup.ID, @@ -98,11 +87,11 @@ func (q *Queries) GetTalkgroupIDsByTags(ctx context.Context, anytags []string, a const getTalkgroupTags = `-- name: GetTalkgroupTags :one SELECT tags FROM talkgroups -WHERE id = systg2id($1, $2) +WHERE system_id = $1 AND tgid = $2 ` -func (q *Queries) GetTalkgroupTags(ctx context.Context, sys int, tg int) ([]string, error) { - row := q.db.QueryRow(ctx, getTalkgroupTags, sys, tg) +func (q *Queries) GetTalkgroupTags(ctx context.Context, systemID int32, tgID int32) ([]string, error) { + row := q.db.QueryRow(ctx, getTalkgroupTags, systemID, tgID) var tags []string err := row.Scan(&tags) return tags, err @@ -117,7 +106,7 @@ JOIN systems sys ON tg.system_id = sys.id WHERE (tg.system_id, tg.tgid) = ($1, $2) UNION SELECT -tgl.id::INT8, tgl.system_id::INT4, tgl.tgid::INT4, tgl.name, +NULL::UUID, tgl.system_id::INT4, tgl.tgid::INT4, tgl.name, tgl.alpha_tag, tgl.alpha_tag, NULL::INTEGER, NULL::JSONB, CASE WHEN tgl.alpha_tag IS NULL THEN NULL ELSE ARRAY[tgl.alpha_tag] END, TRUE, NULL::JSONB, 1.0, sys.id, sys.name, @@ -362,12 +351,12 @@ func (q *Queries) GetTalkgroupsWithLearnedBySystem(ctx context.Context, system i } const setTalkgroupTags = `-- name: SetTalkgroupTags :exec -UPDATE talkgroups SET tags = $3 -WHERE id = systg2id($1, $2) +UPDATE talkgroups SET tags = $1 +WHERE system_id = $2 AND tgid = $3 ` -func (q *Queries) SetTalkgroupTags(ctx context.Context, sys int, tg int, tags []string) error { - _, err := q.db.Exec(ctx, setTalkgroupTags, sys, tg, tags) +func (q *Queries) SetTalkgroupTags(ctx context.Context, tags []string, systemID int32, tgID int32) error { + _, err := q.db.Exec(ctx, setTalkgroupTags, tags, systemID, tgID) return err } diff --git a/pkg/database/talkgroups.sql_test.go b/pkg/database/talkgroups.sql_test.go index 10f9888..14c9215 100644 --- a/pkg/database/talkgroups.sql_test.go +++ b/pkg/database/talkgroups.sql_test.go @@ -6,34 +6,16 @@ import ( "github.com/stretchr/testify/require" ) -const getTalkgroupsWithLearnedByPackedIDsTest = `-- name: GetTalkgroupsWithLearnedByPackedIDs :many -SELECT -tg.id, tg.system_id, tg.tgid, tg.name, tg.alpha_tag, tg.tg_group, tg.frequency, tg.metadata, tg.tags, tg.alert, tg.alert_config, tg.weight, sys.id, sys.name, -FALSE learned -FROM talkgroups tg -JOIN systems sys ON tg.system_id = sys.id -WHERE tg.id = ANY($1::INT8[]) -UNION -SELECT -tgl.id::INT8, tgl.system_id::INT4, tgl.tgid::INT4, tgl.name, -tgl.alpha_tag, tgl.alpha_tag, NULL::INTEGER, NULL::JSONB, -CASE WHEN tgl.alpha_tag IS NULL THEN NULL ELSE ARRAY[tgl.alpha_tag] END, -TRUE, NULL::JSONB, 1.0, sys.id, sys.name, -TRUE learned -FROM talkgroups_learned tgl -JOIN systems sys ON tgl.system_id = sys.id -WHERE systg2id(tgl.system_id, tgl.tgid) = ANY($1::INT8[]) AND ignored IS NOT TRUE -` const getTalkgroupWithLearnedTest = `-- name: GetTalkgroupWithLearned :one SELECT tg.id, tg.system_id, tg.tgid, tg.name, tg.alpha_tag, tg.tg_group, tg.frequency, tg.metadata, tg.tags, tg.alert, tg.alert_config, tg.weight, sys.id, sys.name, FALSE learned FROM talkgroups tg JOIN systems sys ON tg.system_id = sys.id -WHERE tg.id = systg2id($1, $2) +WHERE (tg.system_id, tg.tgid) = ($1, $2) UNION SELECT -tgl.id::INT8, tgl.system_id::INT4, tgl.tgid::INT4, tgl.name, +NULL::UUID, tgl.system_id::INT4, tgl.tgid::INT4, tgl.name, tgl.alpha_tag, tgl.alpha_tag, NULL::INTEGER, NULL::JSONB, CASE WHEN tgl.alpha_tag IS NULL THEN NULL ELSE ARRAY[tgl.alpha_tag] END, TRUE, NULL::JSONB, 1.0, sys.id, sys.name, @@ -52,7 +34,7 @@ JOIN systems sys ON tg.system_id = sys.id WHERE tg.system_id = $1 UNION SELECT -tgl.id::INT8, tgl.system_id::INT4, tgl.tgid::INT4, tgl.name, +NULL::UUID, tgl.system_id::INT4, tgl.tgid::INT4, tgl.name, tgl.alpha_tag, tgl.alpha_tag, NULL::INTEGER, NULL::JSONB, CASE WHEN tgl.alpha_tag IS NULL THEN NULL ELSE ARRAY[tgl.alpha_tag] END, TRUE, NULL::JSONB, 1.0, sys.id, sys.name, @@ -70,7 +52,7 @@ FROM talkgroups tg JOIN systems sys ON tg.system_id = sys.id UNION SELECT -tgl.id::INT8, tgl.system_id::INT4, tgl.tgid::INT4, tgl.name, +NULL::UUID, tgl.system_id::INT4, tgl.tgid::INT4, tgl.name, tgl.alpha_tag, tgl.alpha_tag, NULL::INTEGER, NULL::JSONB, CASE WHEN tgl.alpha_tag IS NULL THEN NULL ELSE ARRAY[tgl.alpha_tag] END, TRUE, NULL::JSONB, 1.0, sys.id, sys.name, @@ -81,7 +63,6 @@ WHERE ignored IS NOT TRUE ` func TestQueryColumnsMatch(t *testing.T) { - require.Equal(t, getTalkgroupsWithLearnedByPackedIDsTest, getTalkgroupsWithLearnedByPackedIDs) require.Equal(t, getTalkgroupWithLearnedTest, getTalkgroupWithLearned) require.Equal(t, getTalkgroupsWithLearnedBySystemTest, getTalkgroupsWithLearnedBySystem) require.Equal(t, getTalkgroupsWithLearnedTest, getTalkgroupsWithLearned) diff --git a/pkg/talkgroups/cache.go b/pkg/talkgroups/cache.go index 0e86b2c..84bc070 100644 --- a/pkg/talkgroups/cache.go +++ b/pkg/talkgroups/cache.go @@ -39,8 +39,8 @@ type Store interface { // Hint hints the Store that the provided talkgroups will be asked for. Hint(ctx context.Context, tgs []ID) error - // Load loads the provided packed talkgroup IDs into the Store. - Load(ctx context.Context, tgs []database.TalkgroupT) error + // Load loads the provided talkgroup ID tuples into the Store. + Load(ctx context.Context, tgs database.TGTuples) error // Invalidate invalidates any caching in the Store. Invalidate() @@ -98,19 +98,20 @@ func NewCache() Store { func (t *cache) Hint(ctx context.Context, tgs []ID) error { t.RLock() - var toLoad []database.TalkgroupT + var toLoad database.TGTuples if len(t.tgs) > len(tgs)/2 { // TODO: instrument this for _, tg := range tgs { _, ok := t.tgs[tg] if !ok { - toLoad = append(toLoad, tg.Tuple()) + toLoad.Append(tg.System, tg.Talkgroup) } } } else { - toLoad = make([]database.TalkgroupT, 0, len(tgs)) + toLoad[0] = make([]uint32, 0, len(tgs)) + toLoad[1] = make([]uint32, 0, len(tgs)) for _, g := range tgs { - toLoad = append(toLoad, g.Tuple()) + toLoad.Append(g.System, g.Talkgroup) } } @@ -136,7 +137,7 @@ func (t *cache) add(rec *Talkgroup) error { type row interface { database.GetTalkgroupsRow | database.GetTalkgroupsWithLearnedRow | - database.GetTalkgroupsWithLearnedBySystemRow + database.GetTalkgroupsWithLearnedBySystemRow | database.GetTalkgroupWithLearnedRow GetTalkgroup() database.Talkgroup GetSystem() database.System GetLearned() bool @@ -180,7 +181,7 @@ func (t *cache) TGs(ctx context.Context, tgs IDs) ([]*Talkgroup, error) { } t.RUnlock() - tgRecords, err := database.FromCtx(ctx).GetTalkgroupsWithLearnedByPackedIDs(ctx, toGet.Tuples()) + tgRecords, err := database.FromCtx(ctx).GetTalkgroupsWithLearnedBySysTGID(ctx, toGet.Tuples()) if err != nil { return nil, err } @@ -196,8 +197,8 @@ func (t *cache) TGs(ctx context.Context, tgs IDs) ([]*Talkgroup, error) { return addToRowList(t, r, tgRecords) } -func (t *cache) Load(ctx context.Context, tgs []database.TalkgroupT) error { - tgRecords, err := database.FromCtx(ctx).GetTalkgroupsWithLearnedByPackedIDs(ctx, tgs) +func (t *cache) Load(ctx context.Context, tgs database.TGTuples) error { + tgRecords, err := database.FromCtx(ctx).GetTalkgroupsWithLearnedBySysTGID(ctx, tgs) if err != nil { return err } @@ -245,7 +246,7 @@ func (t *cache) TG(ctx context.Context, tg ID) (*Talkgroup, error) { return rec, nil } - recs, err := database.FromCtx(ctx).GetTalkgroupsWithLearnedByPackedIDs(ctx, []database.TalkgroupT{tg.Tuple()}) + record, err := database.FromCtx(ctx).GetTalkgroupWithLearned(ctx, int32(tg.System), int32(tg.Talkgroup)) switch err { case nil: case pgx.ErrNoRows: @@ -255,17 +256,13 @@ func (t *cache) TG(ctx context.Context, tg ID) (*Talkgroup, error) { return nil, errors.Join(ErrNotFound, err) } - if len(recs) < 1 { - return nil, ErrNotFound - } - - err = t.add(rowToTalkgroup(recs[0])) + err = t.add(rowToTalkgroup(record)) if err != nil { log.Error().Err(err).Msg("TG() cache add") - return rowToTalkgroup(recs[0]), errors.Join(ErrNotFound, err) + return rowToTalkgroup(record), errors.Join(ErrNotFound, err) } - return rowToTalkgroup(recs[0]), nil + return rowToTalkgroup(record), nil } func (t *cache) SystemName(ctx context.Context, id int) (name string, has bool) { diff --git a/pkg/talkgroups/talkgroup.go b/pkg/talkgroups/talkgroup.go index 5849ebb..c609f42 100644 --- a/pkg/talkgroups/talkgroup.go +++ b/pkg/talkgroups/talkgroup.go @@ -26,13 +26,16 @@ type ID struct { type IDs []ID -func (ids *IDs) Tuples() []database.TalkgroupT { - r := make([]database.TalkgroupT, len(*ids)) - for i := range *ids { - r[i] = (*ids)[i].Tuple() +func (t IDs) Tuples() database.TGTuples { + sys := make([]uint32, len(t)) + tg := make([]uint32, len(t)) + + for i := range t { + sys[i] = t[i].System + tg[i] = t[i].Talkgroup } - return r + return database.TGTuples{sys, tg} } type intId interface { @@ -46,13 +49,6 @@ func TG[T intId, U intId](sys T, tgid U) ID { } } -func (t ID) Tuple() database.TalkgroupT { - return database.TalkgroupT{ - System: t.System, - Talkgroup: t.Talkgroup, - } -} - func (t ID) String() string { return fmt.Sprintf("%d:%d", t.System, t.Talkgroup) diff --git a/sql/postgres/migrations/001_initial.up.sql b/sql/postgres/migrations/001_initial.up.sql index 4706fe9..f412f28 100644 --- a/sql/postgres/migrations/001_initial.up.sql +++ b/sql/postgres/migrations/001_initial.up.sql @@ -23,31 +23,10 @@ CREATE TABLE IF NOT EXISTS systems( name TEXT NOT NULL ); -CREATE OR REPLACE FUNCTION systg2id(_sys INTEGER, _tg INTEGER) RETURNS INT8 LANGUAGE plpgsql AS -$$ -BEGIN - RETURN ((_sys::BIGINT << 32) | _tg); -END -$$; - -CREATE OR REPLACE FUNCTION tgfromid(_id INT8) RETURNS INTEGER LANGUAGE plpgsql AS -$$ -BEGIN - RETURN (_id & x'ffffffff'::BIGINT); -END -$$; - -CREATE OR REPLACE FUNCTION sysfromid(_id INT8) RETURNS INTEGER LANGUAGE plpgsql AS -$$ -BEGIN - RETURN (_id >> 32); -END -$$; - CREATE TABLE IF NOT EXISTS talkgroups( - id INT8 PRIMARY KEY, - system_id INT4 REFERENCES systems(id) NOT NULL GENERATED ALWAYS AS (id >> 32) STORED, - tgid INT4 NOT NULL GENERATED ALWAYS AS (id & x'ffffffff'::BIGINT) STORED, + id UUID PRIMARY KEY, + system_id INT4 REFERENCES systems(id) NOT NULL, + tgid INT4 NOT NULL, name TEXT, alpha_tag TEXT, tg_group TEXT, @@ -56,9 +35,12 @@ CREATE TABLE IF NOT EXISTS talkgroups( tags TEXT[] NOT NULL DEFAULT '{}', alert BOOLEAN NOT NULL DEFAULT 'true', alert_config JSONB, - weight REAL NOT NULL DEFAULT 1.0 + weight REAL NOT NULL DEFAULT 1.0, + UNIQUE (system_id, tgid) ); +CREATE INDEX talkgroups_system_tgid_idx ON talkgroups (system_id, tgid); + CREATE INDEX IF NOT EXISTS talkgroup_id_tags ON talkgroups USING GIN (tags); CREATE TABLE IF NOT EXISTS talkgroups_learned( diff --git a/sql/postgres/migrations/002_reid.down.sql b/sql/postgres/migrations/002_reid.down.sql deleted file mode 100644 index b27dba3..0000000 --- a/sql/postgres/migrations/002_reid.down.sql +++ /dev/null @@ -1,9 +0,0 @@ -DROP INDEX IF EXISTS talkgroups_system_tgid_idx; - -ALTER TABLE talkgroups ALTER COLUMN id SET DATA TYPE INT8 USING (systg2id(system_id, tgid)); - -ALTER TABLE talkgroups DROP COLUMN IF EXISTS tgid; -ALTER TABLE talkgroups ADD COLUMN IF NOT EXISTS tgid INT4 NOT NULL GENERATED ALWAYS AS (id & x'ffffffff'::BIGINT) STORED, - -ALTER TABLE talkgroups DROP COLUMN IF EXISTS system_id; -ALTER TABLE talkgroups ADD COLUMN IF NOT EXISTS system_id INT4 REFERENCES systems(id) NOT NULL GENERATED ALWAYS AS (id >> 32) STORED; diff --git a/sql/postgres/migrations/002_reid.up.sql b/sql/postgres/migrations/002_reid.up.sql deleted file mode 100644 index b0d91d7..0000000 --- a/sql/postgres/migrations/002_reid.up.sql +++ /dev/null @@ -1,7 +0,0 @@ -ALTER TABLE talkgroups ALTER COLUMN system_id DROP EXPRESSION; - -ALTER TABLE talkgroups ALTER COLUMN tgid DROP EXPRESSION; - -ALTER TABLE talkgroups ALTER COLUMN id SET DATA TYPE UUID USING (gen_random_uuid()); - -CREATE INDEX IF NOT EXISTS talkgroups_system_tgid_idx ON talkgroups (system_id, tgid); diff --git a/sql/postgres/migrations/flattened_initial.sql b/sql/postgres/migrations/flattened_initial.sql deleted file mode 100644 index 1c4397d..0000000 --- a/sql/postgres/migrations/flattened_initial.sql +++ /dev/null @@ -1,157 +0,0 @@ -CREATE TABLE IF NOT EXISTS users( - id SERIAL PRIMARY KEY, - username VARCHAR (255) UNIQUE NOT NULL, - password TEXT NOT NULL, - email TEXT NOT NULL, - is_admin BOOLEAN NOT NULL, - prefs JSONB -); - -CREATE INDEX IF NOT EXISTS users_username_idx ON users(username); - -CREATE TABLE IF NOT EXISTS api_keys( - id SERIAL PRIMARY KEY, - owner INTEGER REFERENCES users(id) NOT NULL, - created_at TIMESTAMP NOT NULL, - expires TIMESTAMP, - disabled BOOLEAN, - api_key TEXT UNIQUE NOT NULL -); - -CREATE TABLE IF NOT EXISTS systems( - id INTEGER PRIMARY KEY, - name TEXT NOT NULL -); - -CREATE OR REPLACE FUNCTION systg2id(_sys INTEGER, _tg INTEGER) RETURNS INT8 LANGUAGE plpgsql AS -$$ -BEGIN - RETURN ((_sys::BIGINT << 32) | _tg); -END -$$; - -CREATE OR REPLACE FUNCTION tgfromid(_id INT8) RETURNS INTEGER LANGUAGE plpgsql AS -$$ -BEGIN - RETURN (_id & x'ffffffff'::BIGINT); -END -$$; - -CREATE OR REPLACE FUNCTION sysfromid(_id INT8) RETURNS INTEGER LANGUAGE plpgsql AS -$$ -BEGIN - RETURN (_id >> 32); -END -$$; - -CREATE TABLE IF NOT EXISTS talkgroups( - id UUID PRIMARY KEY, - system_id INT4 REFERENCES systems(id) NOT NULL, - tgid INT4 NOT NULL, - name TEXT, - alpha_tag TEXT, - tg_group TEXT, - frequency INTEGER, - metadata JSONB, - tags TEXT[] NOT NULL DEFAULT '{}', - alert BOOLEAN NOT NULL DEFAULT 'true', - alert_config JSONB, - weight REAL NOT NULL DEFAULT 1.0 -); - -CREATE INDEX talkgroups_system_tgid_idx ON talkgroups (system_id, tgid); - -CREATE INDEX IF NOT EXISTS talkgroup_id_tags ON talkgroups USING GIN (tags); - -CREATE TABLE IF NOT EXISTS talkgroups_learned( - id SERIAL PRIMARY KEY, - system_id INTEGER REFERENCES systems(id) NOT NULL, - tgid INTEGER NOT NULL, - name TEXT NOT NULL, - alpha_tag TEXT, - ignored BOOLEAN, - UNIQUE (system_id, tgid, name) -); - -CREATE TABLE IF NOT EXISTS alerts( - id UUID PRIMARY KEY, - time TIMESTAMPTZ NOT NULL, - tgid INTEGER NOT NULL, - system_id INTEGER REFERENCES systems(id) NOT NULL, - weight REAL, - score REAL, - orig_score REAL, - notified BOOLEAN NOT NULL DEFAULT 'false', - metadata JSONB -); - -CREATE OR REPLACE FUNCTION learn_talkgroup() -RETURNS TRIGGER AS $$ -BEGIN - IF NOT EXISTS ( - SELECT tg.system_id, tg.tgid, tg.name, tg.alpha_tag FROM talkgroups tg WHERE tg.system_id = NEW.system AND tg.tgid = NEW.talkgroup - UNION - SELECT tgl.system_id, tgl.tgid, tgl.name, tgl.alpha_tag FROM talkgroups_learned tgl WHERE tgl.system_id = NEW.system AND tgl.tgid = NEW.talkgroup - ) THEN - INSERT INTO talkgroups_learned(system_id, tgid, name, alpha_tag) VALUES( - NEW.system, NEW.talkgroup, NEW.tg_label, NEW.tg_alpha_tag - ) ON CONFLICT DO NOTHING; - END IF; - RETURN NEW; -END -$$ LANGUAGE plpgsql; - -CREATE TABLE IF NOT EXISTS calls( - id UUID PRIMARY KEY, - submitter INTEGER REFERENCES api_keys(id) ON DELETE SET NULL, - system INTEGER NOT NULL, - talkgroup INTEGER NOT NULL, - call_date TIMESTAMPTZ NOT NULL, - audio_name TEXT, - audio_blob BYTEA, - duration INTEGER, - audio_type TEXT, - audio_url TEXT, - frequency INTEGER NOT NULL, - frequencies INTEGER[], - patches INTEGER[], - tg_label TEXT, - tg_alpha_tag TEXT, - tg_group TEXT, - source INTEGER NOT NULL, - transcript TEXT -); - -CREATE OR REPLACE TRIGGER learn_tg AFTER INSERT ON calls -FOR EACH ROW EXECUTE FUNCTION learn_talkgroup(); - -CREATE INDEX IF NOT EXISTS calls_transcript_idx ON calls USING GIN (to_tsvector('english', transcript)); -CREATE INDEX IF NOT EXISTS calls_call_date_tg_idx ON calls(system, talkgroup, call_date); - -CREATE TABLE IF NOT EXISTS settings( - name TEXT PRIMARY KEY, - updated_by INTEGER REFERENCES users(id), - value JSONB -); - -CREATE TABLE IF NOT EXISTS incidents( - id UUID PRIMARY KEY, - name TEXT NOT NULL, - description TEXT, - start_time TIMESTAMP, - end_time TIMESTAMP, - location JSONB, - metadata JSONB -); - -CREATE INDEX IF NOT EXISTS incidents_name_description_idx ON incidents USING GIN ( - (to_tsvector('english', name) || to_tsvector('english', coalesce(description, '')) - ) -); - -CREATE TABLE IF NOT EXISTS incidents_calls( - incident_id UUID REFERENCES incidents(id) ON UPDATE CASCADE ON DELETE CASCADE, - call_id UUID REFERENCES calls(id) ON UPDATE CASCADE, - notes JSONB, - PRIMARY KEY (incident_id, call_id) -); diff --git a/sql/postgres/queries/talkgroups.sql b/sql/postgres/queries/talkgroups.sql index f2e7a1e..894f4d8 100644 --- a/sql/postgres/queries/talkgroups.sql +++ b/sql/postgres/queries/talkgroups.sql @@ -8,25 +8,21 @@ WHERE tags && ARRAY[$1]; -- name: GetTalkgroupIDsByTags :many SELECT system_id, tgid FROM talkgroups -WHERE (tags @> ARRAY[sqlc.arg(anyTags)]) -AND (tags && ARRAY[sqlc.arg(allTags)]) -AND NOT (tags @> ARRAY[sqlc.arg(notTags)]); +WHERE (tags @> ARRAY[@anyTags]) +AND (tags && ARRAY[@allTags]) +AND NOT (tags @> ARRAY[@notTags]); -- name: GetTalkgroupTags :one SELECT tags FROM talkgroups -WHERE id = systg2id($1, $2); +WHERE system_id = @system_id AND tgid = @tg_id; -- name: SetTalkgroupTags :exec -UPDATE talkgroups SET tags = $3 -WHERE id = systg2id($1, $2); - --- name: BulkSetTalkgroupTags :exec -UPDATE talkgroups SET tags = $2 -WHERE id = ANY($1); +UPDATE talkgroups SET tags = @tags +WHERE system_id = @system_id AND tgid = @tg_id; -- name: GetTalkgroup :one SELECT sqlc.embed(talkgroups) FROM talkgroups -WHERE (system_id, tgid) = (@system_id, @tgid); +WHERE (system_id, tgid) = (@system_id, @tg_id); -- name: GetTalkgroupWithLearned :one SELECT @@ -34,17 +30,17 @@ sqlc.embed(tg), sqlc.embed(sys), FALSE learned FROM talkgroups tg JOIN systems sys ON tg.system_id = sys.id -WHERE (tg.system_id, tg.tgid) = (sqlc.arg(system_id), sqlc.arg(tgid)) +WHERE (tg.system_id, tg.tgid) = (@system_id, @tgid) UNION SELECT -tgl.id::INT8, tgl.system_id::INT4, tgl.tgid::INT4, tgl.name, +NULL::UUID, tgl.system_id::INT4, tgl.tgid::INT4, tgl.name, tgl.alpha_tag, tgl.alpha_tag, NULL::INTEGER, NULL::JSONB, CASE WHEN tgl.alpha_tag IS NULL THEN NULL ELSE ARRAY[tgl.alpha_tag] END, TRUE, NULL::JSONB, 1.0, sys.id, sys.name, TRUE learned FROM talkgroups_learned tgl JOIN systems sys ON tgl.system_id = sys.id -WHERE tgl.system_id = sqlc.arg(system_id) AND tgl.tgid = sqlc.arg(tgid) AND ignored IS NOT TRUE; +WHERE tgl.system_id = @system_id AND tgl.tgid = @tgid AND ignored IS NOT TRUE; -- name: GetTalkgroupsWithLearnedBySystem :many SELECT @@ -82,7 +78,7 @@ JOIN systems sys ON tgl.system_id = sys.id WHERE ignored IS NOT TRUE; -- name: GetSystemName :one -SELECT name FROM systems WHERE id = sqlc.arg(system_id); +SELECT name FROM systems WHERE id = @system_id; -- name: UpdateTalkgroup :one UPDATE talkgroups -- 2.47.0 From 9b93243a4bbfbe8e94b120d0fb98569749f377b3 Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Fri, 15 Nov 2024 10:39:33 -0500 Subject: [PATCH 10/17] Remove bunko type override --- sql/sqlc.yaml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/sql/sqlc.yaml b/sql/sqlc.yaml index 55a0959..342e2fb 100644 --- a/sql/sqlc.yaml +++ b/sql/sqlc.yaml @@ -37,7 +37,3 @@ sql: import: "dynatron.me/x/stillbox/internal/jsontypes" type: "Metadata" nullable: true - - column: "(talkgroups.system_id, talkgroups.tgid)" - go_type: - import: "dynatron.me/x/stillbox/pkg/talkgroups" - type: "ID" -- 2.47.0 From 8569ae6d4afa500a58a000b037cdd63373babc19 Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Fri, 15 Nov 2024 10:41:04 -0500 Subject: [PATCH 11/17] Rename to util --- Makefile | 4 ++-- {.bin => util}/dumpdb.sh | 0 2 files changed, 2 insertions(+), 2 deletions(-) rename {.bin => util}/dumpdb.sh (100%) diff --git a/Makefile b/Makefile index 675cd29..1c0af40 100644 --- a/Makefile +++ b/Makefile @@ -37,10 +37,10 @@ coverage: # backup backs up the database without calls backup: - sh .bin/dumpdb.sh + sh util/dumpdb.sh backupplain: - sh .bin/dumpdb.sh -p + sh util/dumpdb.sh -p test: go test -v ./... diff --git a/.bin/dumpdb.sh b/util/dumpdb.sh similarity index 100% rename from .bin/dumpdb.sh rename to util/dumpdb.sh -- 2.47.0 From 641b0c151a7fb106c8877a7afbb114c895fca95f Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Fri, 15 Nov 2024 10:44:33 -0500 Subject: [PATCH 12/17] Replace was too broad --- internal/forms/forms_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/forms/forms_test.go b/internal/forms/forms_test.go index c7108e8..0051ee1 100644 --- a/internal/forms/forms_test.go +++ b/internal/forms/forms_test.go @@ -194,7 +194,7 @@ func TestUnmarshal(t *testing.T) { opts: []forms.Option{forms.WithAcceptBlank()}, }, { - name: "url encoded jsontypes", + name: "url encoded jsontime", r: makeRequest("urlenc.http"), dest: &urlEncTestJT{}, expect: &UrlEncTestJT, @@ -202,14 +202,14 @@ func TestUnmarshal(t *testing.T) { opts: []forms.Option{forms.WithTag("json")}, }, { - name: "url encoded jsontypes with tz", + name: "url encoded jsontime with tz", r: makeRequest("urlenc.http"), dest: &urlEncTestJT{}, expect: &UrlEncTestJT, opts: []forms.Option{forms.WithAcceptBlank(), forms.WithParseTimeInTZ(time.UTC), forms.WithTag("json")}, }, { - name: "url encoded jsontypes with local", + name: "url encoded jsontime with local", r: makeRequest("urlenc.http"), dest: &urlEncTestJT{}, expect: &UrlEncTestJTLocal, -- 2.47.0 From 1cb301acdf77babcca15972ba841cbee95db2655 Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Fri, 15 Nov 2024 10:51:25 -0500 Subject: [PATCH 13/17] No need to log 404 --- pkg/rest/api.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/rest/api.go b/pkg/rest/api.go index 339ed36..cfbd24e 100644 --- a/pkg/rest/api.go +++ b/pkg/rest/api.go @@ -42,6 +42,7 @@ type errResponse struct { func (e *errResponse) Render(w http.ResponseWriter, r *http.Request) error { switch e.Code { + case http.StatusNotFound: default: log.Error().Str("path", r.URL.Path).Err(e.Err).Int("code", e.Code).Str("msg", e.Error).Msg("request failed") } -- 2.47.0 From 0a88e7f42e45631cbae79ea081313455f466621a Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Fri, 15 Nov 2024 11:15:12 -0500 Subject: [PATCH 14/17] before mockery --- pkg/rest/talkgroups.go | 3 +- pkg/talkgroups/{ => importer}/import.go | 19 +- pkg/talkgroups/importer/import_test.go | 66 +++ pkg/talkgroups/importer/testdata/riscon.json | 0 pkg/talkgroups/importer/testdata/riscon.txt | 408 +++++++++++++++++++ 5 files changed, 486 insertions(+), 10 deletions(-) rename pkg/talkgroups/{ => importer}/import.go (84%) create mode 100644 pkg/talkgroups/importer/import_test.go create mode 100644 pkg/talkgroups/importer/testdata/riscon.json create mode 100644 pkg/talkgroups/importer/testdata/riscon.txt diff --git a/pkg/rest/talkgroups.go b/pkg/rest/talkgroups.go index 1a539f1..a392ff1 100644 --- a/pkg/rest/talkgroups.go +++ b/pkg/rest/talkgroups.go @@ -6,6 +6,7 @@ import ( "dynatron.me/x/stillbox/internal/forms" "dynatron.me/x/stillbox/pkg/database" "dynatron.me/x/stillbox/pkg/talkgroups" + "dynatron.me/x/stillbox/pkg/talkgroups/importer" "github.com/go-chi/chi/v5" ) @@ -108,7 +109,7 @@ func (tga *talkgroupAPI) put(w http.ResponseWriter, r *http.Request) { } func (tga *talkgroupAPI) tgImport(w http.ResponseWriter, r *http.Request) { - var impJob talkgroups.ImportJob + var impJob importer.ImportJob err := forms.Unmarshal(r, &impJob, forms.WithTag("json"), forms.WithAcceptBlank(), forms.WithOmitEmpty()) if err != nil { wErr(w, r, badRequest(err)) diff --git a/pkg/talkgroups/import.go b/pkg/talkgroups/importer/import.go similarity index 84% rename from pkg/talkgroups/import.go rename to pkg/talkgroups/importer/import.go index e58a592..667936d 100644 --- a/pkg/talkgroups/import.go +++ b/pkg/talkgroups/importer/import.go @@ -1,4 +1,4 @@ -package talkgroups +package importer import ( "bufio" @@ -15,6 +15,7 @@ import ( "dynatron.me/x/stillbox/internal/jsontypes" "dynatron.me/x/stillbox/pkg/database" + "dynatron.me/x/stillbox/pkg/talkgroups" ) type ImportSource string @@ -28,7 +29,7 @@ var ( ) type importer interface { - importTalkgroups(ctx context.Context, sys int, r io.Reader) ([]Talkgroup, error) + importTalkgroups(ctx context.Context, sys int, r io.Reader) ([]talkgroups.Talkgroup, error) } type ImportJob struct { @@ -39,7 +40,7 @@ type ImportJob struct { importer `json:"-"` } -func (ij *ImportJob) Import(ctx context.Context) ([]Talkgroup, error) { +func (ij *ImportJob) Import(ctx context.Context) ([]talkgroups.Talkgroup, error) { r := bytes.NewReader([]byte(ij.Body)) switch ij.Type { @@ -65,12 +66,12 @@ const ( var rrRE = regexp.MustCompile(`DEC\s+HEX\s+Mode\s+Alpha Tag\s+Description\s+Tag`) -func (rr *radioReferenceImporter) importTalkgroups(ctx context.Context, sys int, r io.Reader) ([]Talkgroup, error) { +func (rr *radioReferenceImporter) importTalkgroups(ctx context.Context, sys int, r io.Reader) ([]talkgroups.Talkgroup, error) { sc := bufio.NewScanner(r) - tgs := make([]Talkgroup, 0, 8) - sysn, has := StoreFrom(ctx).SystemName(ctx, sys) + tgs := make([]talkgroups.Talkgroup, 0, 8) + sysn, has := talkgroups.StoreFrom(ctx).SystemName(ctx, sys) if !has { - return nil, ErrNoSuchSystem + return nil, talkgroups.ErrNoSuchSystem } importedFrom := jsontypes.Metadata{ @@ -109,14 +110,14 @@ func (rr *radioReferenceImporter) importTalkgroups(ctx context.Context, sys int, metadata := jsontypes.Metadata{ "imported": importedFrom, } - tgt := TG(sys, tgid) + tgt := talkgroups.TG(sys, tgid) mode := fields[2] if strings.Contains(mode, "E") { metadata["encrypted"] = true } tags := []string{fields[5]} gn := groupName // must take a copy - tgs = append(tgs, Talkgroup{ + tgs = append(tgs, talkgroups.Talkgroup{ Talkgroup: database.Talkgroup{ ID: uuid.New(), Tgid: int32(tgt.Talkgroup), diff --git a/pkg/talkgroups/importer/import_test.go b/pkg/talkgroups/importer/import_test.go new file mode 100644 index 0000000..d281496 --- /dev/null +++ b/pkg/talkgroups/importer/import_test.go @@ -0,0 +1,66 @@ +package importer_test + +import ( + "context" + "encoding/json" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "dynatron.me/x/stillbox/pkg/talkgroups/importer" +) + +func getFixture(fixture string) []byte { + fixt, err := os.ReadFile("testdata/" + fixture) + if err != nil { + panic(err) + } + + + return fixt +} + +func TestRadioReferenceImport(t *testing.T) { + ctx := context.Background() + tests := []struct{ + name string + input []byte + sysID int + jsExpect []byte + expectErr error + }{ + { + name: "base", + input: getFixture("riscon.txt"), + jsExpect: getFixture("riscon.json"), + sysID: 197, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + ij := &importer.ImportJob{ + Type: "radioreference", + SystemID: tc.sysID, + Body: string(tc.input), + } + + tgs, err := ij.Import(ctx) + + if tc.expectErr != nil { + require.Error(t, err) + assert.Contains(t, err.Error(), tc.expectErr.Error()) + } else { + require.NoError(t, err) + + jse, jerr := json.Marshal(tgs) + require.NoError(t, jerr) + + assert.Equal(t, tc.jsExpect, jse) + } + + }) + } +} diff --git a/pkg/talkgroups/importer/testdata/riscon.json b/pkg/talkgroups/importer/testdata/riscon.json new file mode 100644 index 0000000..e69de29 diff --git a/pkg/talkgroups/importer/testdata/riscon.txt b/pkg/talkgroups/importer/testdata/riscon.txt new file mode 100644 index 0000000..1f024af --- /dev/null +++ b/pkg/talkgroups/importer/testdata/riscon.txt @@ -0,0 +1,408 @@ +Statewide Mutual Aid/Intersystem +DEC HEX Mode Alpha Tag Description Tag +2 002 D Intercity FD Intercity Fire Interop +3 003 D Intercity PD Intercity Police Interop +State Police - District A (North) + +District A comprises barracks in Lincoln Woods and Scituate in the northern region of the state +DEC HEX Mode Alpha Tag Description Tag +21 015 D RISP N Disp North Dispatch Law Dispatch +22 016 DE RISP N Car North Car-to-Car/Information Law Talk +24 018 DE RISP N Tac 1 North Tactical Ops 1 Law Tac +23 017 DE RISP N Tac 2 North Tactical Ops 2 Law Tac +State Police - District B (South) + +District B comprises barracks in Hope Valley, Wickford as well as detail assignments at TF Green Airport and Block Island in the southern region of the state +DEC HEX Mode Alpha Tag Description Tag +25 019 D RISP S Disp South Dispatch Law Dispatch +27 01b DE RISP S Car South Car-to-Car/Information Law Talk +Statewide Fire +DEC HEX Mode Alpha Tag Description Tag +16 010 D State FMO State Fire Marshall Fire-Talk +1038 40e D NRI Fire Chi Northern Rhode Island Fire Chiefs Fire-Talk +1041 411 D SRI Fire Chi Southern Rhode Island Fire Chiefs Fire-Talk +1314 522 D Tanker TF 1 Tanker Taskforce 1 Fire-Talk +Statewide EMS and Hospitals +DEC HEX Mode Alpha Tag Description Tag +194 0c2 D Lifepact Amb Lifepact Ambulance (Statewide) EMS Dispatch +212 0d4 D Fatima-St Joes Fatima St Josephs Business +220 0dc DE Health 1 Health 1 EMS-Talk +221 0dd DE Health 2 Health 2 EMS-Talk +222 0de D Dept of HealthSW Department of Health - Statewide EMS-Talk +228 0e4 DE DMAT South DMAT South Emergency Ops +232 0e8 D Life Span 1 Life Span Net 1 EMS-Tac +234 0ea D RI Hosp Ops RI Hospital Operations Business +Department of Environmental Management +DEC HEX Mode Alpha Tag Description Tag +120 078 D DEM PD Ops Law Enforcement Operations Law Dispatch +122 07a D DEM Police Law Enforcement Police Law Talk +Emergency Management Agency +DEC HEX Mode Alpha Tag Description Tag +10 00a D EMA-1 Emergency Management Agency 1 Emergency Ops +20 014 D EMA Emergency Management Agency Emergency Ops +Statewide Area/Events +DEC HEX Mode Alpha Tag Description Tag +4 004 D Wide Area 3 Wide Area 3 Interop +5 005 D Wide Area 4 Wide Area 4 Interop +6 006 D Wide Area 5 Wide Area 5 Interop +7 007 D Wide Area 6 Wide Area 6 Interop +1018 3fa D SOUTHWIDE 1 Southwide CH-1 Interop +1019 3fb D SOUTHWIDE 2 Southwide CH-2 Interop +1022 3fe D WIDE AREA 7 Wide Area 7 Interop +1023 3ff DE WIDE AREA 8 Wide Area 8 Interop +1025 401 D Inland Marine IO Inland Marine Interop Interop +1037 40d DE SOUTHSIDE 5 Southside CH 5 Interop +1173 495 D NORTHWIDE1 North Wide 1 Interop +1174 496 D NORTHWIDE2 North Wide 2 Interop +1177 499 DE NORTHWIDE5 North Wide 5 Interop +1185 4a1 D METROWIDE1 Metro Wide 1 Interop +1186 4a2 D METROWIDE2 Metro Wide 2 Interop +1187 4a3 DE METROWIDE3 Metro Wide 3 Interop +1335 537 D EASTWIDE 1 East Wide 1 Interop +1336 538 D EASTWIDE 2 East Wide 2 Interop +1337 539 DE EASTWIDE 3 East Wide 3 Interop +11186 2bb2 D METROWIDE2 Metro Wide 2 Interop +Statewide Emergency Response +DEC HEX Mode Alpha Tag Description Tag +1033 409 D TANK TF Tanker Taskforce Fire-Tac +1034 40a D HZT DC1 Hazmat 1 Fire-Tac +1035 40b D HZT DC2 Hazmat 2 Fire-Tac +Department of Transportation +DEC HEX Mode Alpha Tag Description Tag +176 0b0 D RIDOT Primary Department of Transportation - Primary Public Works +Tunnel and Bridge Authority +DEC HEX Mode Alpha Tag Description Tag +4421 1145 D RITBA - Pell Bdg Newport Pell Bridge Operations Public Works +Federal +DEC HEX Mode Alpha Tag Description Tag +274 112 D VA Police Providence VA Police Law Dispatch +RIPTA +DEC HEX Mode Alpha Tag Description Tag +186 0ba DE RIPTA Rhode Island Public Transit Auth. Transportation +187 0bb D RIPTA Rhode Island Public Transit Auth. Transportation +188 0bc D RIPTA Rhode Island Public Transit Auth. Transportation +189 0bd D RIPTA Rhode Island Public Transit Auth. Transportation +190 0be D RIPTA Rhode Island Public Transit. Auth. Transportation +Quonset ANGB +DEC HEX Mode Alpha Tag Description Tag +304 130 D Quonset ANGB FD Fire Operations Fire Dispatch +Rhode Island Airport Commission +DEC HEX Mode Alpha Tag Description Tag +17 011 DE TF Green PD Airport Police Operations Law Dispatch +19 013 D TF Green FD Airport Fire Operations Fire Dispatch +College/Education Security +DEC HEX Mode Alpha Tag Description Tag +1126 466 DE URI PD University of Rhode Island Police - Dispatch Law Dispatch +1131 46b DE URI EMS University of Rhode Island - EMS EMS Dispatch +1348 544 D St George Sec St. George's School (Middletown) - Security Security +10228 27f4 DE RISD Secuty Rhode Island School of Design - Security Security +10229 27f5 DE PROV COLL Providence College Security - Dispatch Security +10230 27f6 D RI COL SEC Rhode Island College Security Security +11001 2af9 DE BROWN UNIV Brown University Police - Dispatch Law Dispatch +11002 2afa DE BROWN CAR Brown University Police - Car-to-Car Law Talk +11003 2afb DE BROWN TAC Brown University Police - Tactical Law Tac +Statewide Misc. +DEC HEX Mode Alpha Tag Description Tag +12 00c DE METROWIDE2 Metro Wide 2 Interop +14 00e DE METROWIDE4 Metro Wide 4 Interop +70 046 DE TFC TRIBUNAL RI Traffic Tribunal Security Security +168 0a8 D Red Cross 1 Rhode Island Red Cross - Primary Other +169 0a9 D Red Cross 2 Rhode Island Red Cross - Secondary Other +223 0df D NURSING HM Statewide Nursing Homes Net Other +243 0f3 D Slater Hosp Ops Hospital Operations Business +244 0f4 D Slater Hosp Sec Slater Hospital Security Security +Washington County +DEC HEX Mode Alpha Tag Description Tag +1042 412 D WashCo FireG County Fireground Fire-Tac +1479 5c7 D WashCo FireS County Fire Station/Station Fire-Talk +Barrington +DEC HEX Mode Alpha Tag Description Tag +1712 6b0 D BarringtnFD1 Fire 1 Dispatch Fire Dispatch +1713 6b1 D BarringtnFD2 Fire 2 Fire-Tac +1715 6b3 DE BarringtonPD 1 Police Operations Law Dispatch +1716 6b4 D BarringtonPD 2 Police Secondary Law Tac +Bristol +DEC HEX Mode Alpha Tag Description Tag +1744 6d0 D Bristol FD Fire Operations (Patch from VHF) Fire Dispatch +1755 6db D Bristol Harbor Harbormaster Public Works +Burrillville +DEC HEX Mode Alpha Tag Description Tag +2003 7d3 D Burrville PD Police Law Dispatch +2004 7d4 D Burrvl PD2 Police 2 Law Talk +2005 7d5 DE Burrvl PD3 Police 3 Detectives Law Tac +2006 7d6 D Burrvl PD4 Police 4 Law Tac +2000 7d0 D Burrvl FD Fire Misc (Ops are VHF) Fire-Tac +2001 7d1 D Burvl FDTAC1 Fire TAC-1 Fire-Tac +2009 7d9 D Burvl FDTAC2 Fire TAC-2 Fire-Tac +2002 7d2 D Burrvl EMS EMS Misc (Ops are VHF) EMS-Tac +2007 7d7 D Burrvl Town Town-Wide Multi-Tac +2008 7d8 D Burrvl EMA Emergency Management Emergency Ops +Central Falls +DEC HEX Mode Alpha Tag Description Tag +1838 72e D CentFallsPD1 Police 1 Dispatch Law Dispatch +1839 72f D CentFallsPD2 Police 2 Law Dispatch +1835 72b D CentFalls FD 1 Fire Dispatch (Simulcast of UHF) Fire Dispatch +1836 72c D CentFalls FD 2 Fireground Fire-Tac +Charlestown +DEC HEX Mode Alpha Tag Description Tag +1425 591 D CharlestownPD Police Operations - Simulcast of UHF Law Dispatch +1429 595 D Chastown EMS EMS - Linked to 151.3325 EMS Dispatch +Coventry +DEC HEX Mode Alpha Tag Description Tag +1483 5cb D Coventry PD Police 1 - Dispatch Law Dispatch +1484 5cc D Coventry PD2 Police 2 Law Tac +1480 5c8 D Coventry FD Fire Fire Dispatch +Cranston +DEC HEX Mode Alpha Tag Description Tag +1500 5dc D Cranston FD Disp Fire - Dispatch/Operations Fire Dispatch +1501 5dd D Cranston FD FG2 Fire - Fireground 2 Fire-Tac +1502 5de D Cranston FD FG3 Fire - Fireground 3 Fire-Tac +1503 5df D Cranston FD FG4 Fire - Fireground 4 Fire-Talk +1504 5e0 D Cranston FD Admi Fire - Admin/Alt Fireground 5 Fire-Talk +Cumberland +DEC HEX Mode Alpha Tag Description Tag +1520 5f0 D Cumberland FD Fire Fire Dispatch +1523 5f3 D Cumberland PD Police Secondary Law Dispatch +East Greenwich +DEC HEX Mode Alpha Tag Description Tag +1776 6f0 D E Greenwich F-TA Fire Talk Around Fire-Talk +1779 6f3 D E Greenwich PD Police Operations Law Dispatch +East Providence +DEC HEX Mode Alpha Tag Description Tag +1869 74d D E Prov PD 1 Police 1 - Dispatch Law Dispatch +1872 750 DE E Prov PD 2 Police 2 Law Talk +1870 74e DE E Prov PD 3 Police 3 Law Talk +1883 75b DE E Prov PD12 Detectives Law Talk +1866 74a D E Prov FD 1 Fire - Dispatch/Operations Fire Dispatch +1867 74b D E Prov FD 2 Fire "Channel 2" Fire-Tac +1878 756 D E Prov FD 3 Fire "Channel 3" Fire-Tac +Exeter +DEC HEX Mode Alpha Tag Description Tag +2064 810 D Exeter FD-G Fire - Fireground Fire-Tac +Foster +DEC HEX Mode Alpha Tag Description Tag +1904 770 D Foster Fire Fire Fire Dispatch +Glocester +DEC HEX Mode Alpha Tag Description Tag +1939 793 D Glocester PD Police Law Dispatch +1940 794 D Glocester PD 2 Police Secondary Law Tac +Hopkinton +DEC HEX Mode Alpha Tag Description Tag +1410 582 DE Hopkinton PD Police Law Dispatch +Jamestown +DEC HEX Mode Alpha Tag Description Tag +1100 44c DE Jamestown PD 1 Police 1 - Dispatch Law Dispatch +1101 44d DE Jamestown PD 2 Police 2 Law Dispatch +1108 454 D Jamestown FD Fire Fire Dispatch +1120 460 D Jamestown FG 1 Fireground 1 Fire-Tac +1121 461 D Jamestown FG 2 Fireground 2 Fire-Tac +1114 45a D Jamestown DPW Public Works Public Works +1107 453 D Jamestown School Town Schools Schools +Johnston +DEC HEX Mode Alpha Tag Description Tag +1619 653 DE Johnston PD Police Operations Law Dispatch +1616 650 D Johnston FD Fire Operations Fire Dispatch +1617 651 D Johnston FG Fireground Fire-Tac +Lincoln +DEC HEX Mode Alpha Tag Description Tag +1683 693 D Lincoln Police Police F1 Law Dispatch +1684 694 D Lincoln Police 2 Police F2 Law Tac +1680 690 D Lincoln Fire 1 Fire Dispatch Fire Dispatch +1681 691 D Lincoln Fire 2 Fireground 2 Fire-Tac +1691 69b D Lincoln Fire 3 Fireground 3 Fire-Tac +1682 692 D Lincoln EMS EMS EMS Dispatch +1688 698 D Lincoln EMA Emergency Management Emergency Ops +1687 697 D Lincoln Townwide Townwide Interop +1692 69c D Lincoln DPW Public Works Public Works +Little Compton +DEC HEX Mode Alpha Tag Description Tag +1264 4f0 D LittleCompPD Police Law Dispatch +1266 4f2 D LittleCompFD Fire Fire Dispatch +Middletown +DEC HEX Mode Alpha Tag Description Tag +1338 53a D MiddletownPD Police Operations Law Dispatch +1343 53f D Middletown FD Fire Operations Fire Dispatch +1345 541 D MiddletownTW Townwide Multi-Dispatch +Narragansett +DEC HEX Mode Alpha Tag Description Tag +1001 3e9 DE Narrag PD 1 Police - Dispatch Law Dispatch +1002 3ea DE Narrag PD 2 Police - Car/Car Law Talk +1003 3eb DE Narrag PD 3 Police - Special Details 1/Town Beaches Law Tac +1004 3ec DE Narrag PD 4 Police - Special Details 2 Law Tac +1005 3ed DE Narrag PD 5 Police - Harbormaster Law Talk +1007 3ef DE Narrag PD 7 Police - Detectives Law Talk +1008 3f0 DE Narrag PD 8 Police - Detectives Law Talk +1006 3ee D Narrag FD Fire - Dispatch Fire Dispatch +1012 3f4 D Narrag FDFG1 Fire - Fireground 1 Fire-Tac +1013 3f5 D Narrag FDFG2 Fire - Fireground 2 Fire-Tac +1016 3f8 D Narrag FD AD Fire - Administration Fire-Talk +1014 3f6 D Narrag EMS Fire - EMS Ops EMS Dispatch +1017 3f9 D Narrag DPW Public Works Public Works +1010 3f2 D Narrag TownA Town Administration Other +1011 3f3 D Narrag IOP Townwide Interop Interop +New Shoreham + +New Shoreham is on Block Island. New Shoreham operates primarily on their own Capacity Plus trunk. +DEC HEX Mode Alpha Tag Description Tag +1376 560 D New Shore PD Police Law Dispatch +Newport +DEC HEX Mode Alpha Tag Description Tag +1300 514 DE Newport PD 1 Police 1 - Dispatch Law Dispatch +1302 516 DE Newport PD 2 Police 2 - Records Law Talk +1304 518 DE Newport PD 4 Police 4 - Tactical 1 Law Talk +1307 51b DE Newport PD 7 Police 7 - Tactical 4 Law Talk +1308 51c DE Newport PD 8 Police 8 - Tactical 5 Law Talk +1303 517 D Newport FD1 Fire Dispatch/Operations Fire Dispatch +1305 519 D Newport FG1 Fireground Ops 1 Fire-Tac +1306 51a D Newport FG2 Fireground Ops 2 Fire-Tac +1301 515 D Newport FDT Fire - Training Fire-Talk +1291 50b D Newport Water Water Department Public Works +1293 50d D Newport DPW Public Works Public Works +1297 511 D Newport Evnt Citywide Events Public Works +1312 520 D Newport CW Newport Citywide Interop +North Kingstown +DEC HEX Mode Alpha Tag Description Tag +1285 505 D NKing PD 1 Police 1 - Dispatch Law Dispatch +1286 506 DE NKing PD 2 Police 2 - Admin Law Talk +1287 507 De NKing PD 3 Police 3 - Car/Car Law Tac +1280 500 D NKing Fire D Fire - Dispatch Fire Dispatch +1281 501 D NKing Fire G Fire - Fireground Fire-Tac +North Providence +DEC HEX Mode Alpha Tag Description Tag +1536 600 DE NorthPrv PD1 Police 1 - Dispatch Law Dispatch +1537 601 DE NorthPrv PD2 Police 2 - Car/Car Law Talk +1538 602 DE NorthPrv PD3 Police 3 - Tactical Law Tac +1547 60b D NorthPrv FDD Fire Dispatch Fire Dispatch +1548 60c D NorthPrv Fire 2 Fire 2 Fire-Tac +1549 60d D NorthPrv Fire 3 Fire 3 Fire-Tac +1550 60e D NorthPrv Fire 4 Fire 4 Fire-Tac +1551 60f D NorthPrv Fire 5 Fire 5 Fire Dispatch +1552 610 DE NorthPrv Fire 6 Fire 6 Fire-Tac +1544 608 D NorthPrv TownW 1 Townwide 1 Interop +1545 609 D NorthPrv TownW 2 Townwide 2 Interop +1554 612 D NorthPrv DPW Public Works Public Works +North Smithfield +DEC HEX Mode Alpha Tag Description Tag +1971 7b3 DE N Smithfd PD Police Law Dispatch +1968 7b0 D N Smithfield FD Fire Dispatch/Operations Fire Dispatch +1969 7b1 D N Smithfield FD2 Fire Secondary Fire-Tac +1981 7bd D N Smithfield FD3 Fireground Fire-Tac +Pawtucket +DEC HEX Mode Alpha Tag Description Tag +1440 5a0 D Pawtucket FD 1 Fire - Operations Fire Dispatch +1441 5a1 D Pawtucket FG Fireground Fire-Tac +1442 5a2 D Pawtucket EMSTac EMS Tac EMS-Tac +Portsmouth +DEC HEX Mode Alpha Tag Description Tag +1248 4e0 DE PortsmouthPD Police Law Dispatch +1253 4e5 D Portsmouth FD Fire Dispatch (Patch to VHF Primary) Fire Dispatch +1255 4e7 D Portsmouth FG Fireground Fire-Tac +1262 4ee D Prudence Isl FD Island Fire Dispatch Fire Dispatch +Providence (City) + +Providence fireground channels may be patched. +As of this writing, +FG 05 (10102) and 02 (10107) are patched +FG 06 (10103) and 03 (10108) are patched +FG 07 (10104) and 04 (10109) are patched. +DEC HEX Mode Alpha Tag Description Tag +10000 2710 D PPD ATG Police - All Call - Emergency Broadcasts Emergency Ops +10001 2711 D PPD CH 1 Police 1 - Dispatch Law Dispatch +10002 2712 DE PPD CH 2 Police 2 Law Talk +10003 2713 DE PPD CH 3 Police 3 Law Talk +10004 2714 DE PPD CH-4 Police 4 Law Talk +10005 2715 DE PPD DETEC 1 Police 5 -Detectives 1 Law Talk +10006 2716 DE PPD T/A Police 6 - Car-to-Car Law Talk +10007 2717 DE PPD NARC 1 Police 7 - Narcotics 1 Law Talk +10008 2718 DE PPD NARC 2 Police 8 - Narcotics 2 Law Tac +10009 2719 DE PPD DETEC 2 Police 9 - Detectives 2 Law Talk +10010 271a DE PPD DETAIL 1 Police 10 - Special Details 1 Law Tac +10011 271b DE PPD DETAIL 2 Police 11 - Special Details 2 Law Tac +10012 271c DE PPD CORR SEC Police 12 - Corrections Security Law Talk +10013 271d DE PPD SRU Police 13 - Special Response Unit Law Tac +10014 271e DE PPD ADMIN Police 14 - Administration Law Talk +10100 2774 D PROV FD ATG Fire All Call - Emergency Broadcasts Emergency Ops +10101 2775 D PFD DISPATCH Fire Dispatch Fire Dispatch +10107 277b D PFD CH-2 FG Fireground 2 Fire-Tac +10108 277c D PFD CH-3 FG Fireground 3 Fire-Tac +10109 277d D PFD CH-4 FG Fireground 4 Fire-Tac +10102 2776 D PFD CH-5 Fire 5 Fire-Tac +10103 2777 D PFD CH-6 Fire 6 Fire-Tac +10104 2778 D PFD CH-7 Fire 7 Fire-Tac +10110 277e D PFD M/A 1 Fire - Mutual Aid 1 Fire-Tac +10111 277f D PFD M/A 2 Fire - Mutual Aid 2 Fire-Tac +10112 2780 D PFD M/A 3 Fire - Mutual Aid 3 Fire-Tac +10113 2781 D PFD Fireground 8 Fireground 8 Fire-Talk +10105 2779 D PFD ADMIN Fire - Administration Fire-Talk +10106 277a D PFD COMM Fire - Communications Fire-Talk +10207 27df D PROV DPW Public Works Public Works +Richmond +DEC HEX Mode Alpha Tag Description Tag +2035 7f3 D Richmond PD Police Law Dispatch +2042 7fa D Chariho Reg HS Chariho Regional High School Schools +Scituate +DEC HEX Mode Alpha Tag Description Tag +1460 5b4 D Scituate PD Police Law Dispatch +1463 5b7 D Scituate FD Fire Operations Fire Dispatch +Smithfield +DEC HEX Mode Alpha Tag Description Tag +1651 673 D SmithfieldPD Police Operations Law Dispatch +1652 674 D Smfld PD 2 Police Secondary Law Tac +1653 675 DE Smfld PD Det Police Detectives Law Tac +1654 676 DE Smfld PD Adm Police Admin Law Talk +1661 67d D Smfld PD Dtl Police Details Law Talk +1648 670 D SmithfieldFD Fire - Fireground Fire-Tac +1655 677 D Smfld Town Town-Wide Multi-Tac +1657 679 D Smfld EMA Emergency Management Emergency Ops +1660 67c D Smfld DPW Public Works Public Works +South Kingstown +DEC HEX Mode Alpha Tag Description Tag +1225 4c9 DE SKing PD 1 Police 1 - Dispatch Law Dispatch +1226 4ca DE SKing PD 2 Police 2 - Car/Car Law Talk +1235 4d3 DE SKing PD 3 Police 3 - Tactical Law Tac +1236 4d4 DE SKing PD 5 Police 5 - Tactical Law Tac +1232 4d0 D SKing FD Lnk Fire - UHF Simulcast Fire Dispatch +1240 4d8 D SKing Fire D Fire - Detail Fire-Talk +1227 4cb D UnionFD FG 1 Union Fire District - Fireground 1 Fire-Tac +1237 4d5 D UnionFD FG 2 Union Fire District - Fireground 2 Fire-Tac +1026 402 D UnionFD Evnt Union Fire District - Special Events Fire-Talk +1015 3f7 DE SKing EMS EMS EMS Dispatch +Tiverton +DEC HEX Mode Alpha Tag Description Tag +1316 524 D Tiverton PD Police (Simulcast 482.9625) Law Dispatch +1315 523 D Tiverton FD Fire (Simulcast 471.7875) Fire Dispatch +Warwick +DEC HEX Mode Alpha Tag Description Tag +1162 48a D Warwick FD Fire Fire Dispatch +1170 492 D Warwick FG Fireground Fire-Tac +West Greenwich +DEC HEX Mode Alpha Tag Description Tag +1805 70d D W Greenwh PD Police Law Dispatch +1806 70e D W GreenwichPD2 Police Secondary Law Tac +West Warwick +DEC HEX Mode Alpha Tag Description Tag +1208 4b8 D W Warwick FD Fire Operations Fire Dispatch +Westerly +DEC HEX Mode Alpha Tag Description Tag +1050 41a DE Westerly PD1 Police 1 - Dispatch Law Dispatch +1051 41b DE Westerly PD2 Police 2 Law Talk +1052 41c DE Westerly PD3 Police 3 Law Talk +1053 41d DE Westerly PD4 Police 4 Law Talk +1054 41e D Westerly PD5 Police 5 - Reserve Officers Law Talk +1064 428 D Westerly PD6 Police 6 - Traffic Division Law Talk +1063 427 D Westerly FD Fire Operations Fire Dispatch +1072 430 D Westerly PFE Police/Fire/EMS Ops Multi-Talk +1082 43a D Westerly EMS EMS Operations EMS Dispatch +Woonsocket +DEC HEX Mode Alpha Tag Description Tag +1363 553 D Woonskt PD 1 Police 1 - Dispatch Law Dispatch +1364 554 DE Woonskt PD 2 Police 2 Law Talk +1360 550 D Woonsocket FD D Fire Dispatch - Operations Fire-Talk +1361 551 D Woonsocket FD 2 Fire Secondary Fire Dispatch +1354 54a D Woonskt FD 3 Fire - Fireground 3 Fire-Tac +1367 557 D Woonskt City Citywide Multi-Talk +1368 558 D Woonsocket PW Public Works - Streets Public Works +Radio Technicians +DEC HEX Mode Alpha Tag Description Tag +1 001 D Radio Techs RISCON Radio Technicians Public Works +10125 278d D Radio Techs RISCON Radio Technicians Public Works -- 2.47.0 From e82f07e0940087caa2ca733165e11be3c7f9c625 Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Fri, 15 Nov 2024 11:34:54 -0500 Subject: [PATCH 15/17] housekeeping --- pkg/alerting/alert/alert.go | 2 +- pkg/alerting/stats.go | 2 +- pkg/calls/filter.go | 2 +- pkg/database/calls.sql.go | 6 +++--- pkg/database/db.go | 2 +- pkg/database/models.go | 8 ++++---- pkg/database/querier.go | 6 +++--- pkg/database/talkgroups.go | 4 ++-- pkg/database/talkgroups.sql.go | 32 ++++++++++++++--------------- pkg/database/users.sql.go | 2 +- pkg/talkgroups/cache.go | 2 +- pkg/talkgroups/importer/import.go | 2 +- sql/postgres/queries/talkgroups.sql | 6 +++--- sql/sqlc.yaml | 3 +++ 14 files changed, 41 insertions(+), 38 deletions(-) diff --git a/pkg/alerting/alert/alert.go b/pkg/alerting/alert/alert.go index 9b83006..de64278 100644 --- a/pkg/alerting/alert/alert.go +++ b/pkg/alerting/alert/alert.go @@ -37,7 +37,7 @@ func (a *Alert) ToAddAlertParams() database.AddAlertParams { ID: a.ID, Time: pgtype.Timestamptz{Time: a.Timestamp, Valid: true}, SystemID: int(a.Score.ID.System), - Tgid: int(a.Score.ID.Talkgroup), + TGID: int(a.Score.ID.Talkgroup), Weight: &a.Weight, Score: &f32score, OrigScore: origScore, diff --git a/pkg/alerting/stats.go b/pkg/alerting/stats.go index 3ace635..2daa497 100644 --- a/pkg/alerting/stats.go +++ b/pkg/alerting/stats.go @@ -49,7 +49,7 @@ func (as *alerter) tgStatsHandler(w http.ResponseWriter, r *http.Request) { tgMap := make(map[talkgroups.ID]database.GetTalkgroupsRow, len(tgs)) for _, t := range tgs { - tgMap[talkgroups.ID{System: uint32(t.System.ID), Talkgroup: uint32(t.Talkgroup.Tgid)}] = t + tgMap[talkgroups.ID{System: uint32(t.System.ID), Talkgroup: uint32(t.Talkgroup.TGID)}] = t } renderData := struct { diff --git a/pkg/calls/filter.go b/pkg/calls/filter.go index ecc1fc6..844c2e6 100644 --- a/pkg/calls/filter.go +++ b/pkg/calls/filter.go @@ -71,7 +71,7 @@ func (f *TalkgroupFilter) compile(ctx context.Context) error { } for _, tg := range tagTGs { - f.talkgroups[tgs.ID{System: uint32(tg.SystemID), Talkgroup: uint32(tg.Tgid)}] = true + f.talkgroups[tgs.ID{System: uint32(tg.SystemID), Talkgroup: uint32(tg.TGID)}] = true } } diff --git a/pkg/database/calls.sql.go b/pkg/database/calls.sql.go index fc56bd0..ab98ae4 100644 --- a/pkg/database/calls.sql.go +++ b/pkg/database/calls.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.26.0 +// sqlc v1.27.0 // source: calls.sql package database @@ -31,7 +31,7 @@ VALUES type AddAlertParams struct { ID uuid.UUID `json:"id"` Time pgtype.Timestamptz `json:"time"` - Tgid int `json:"tgid"` + TGID int `json:"tgid"` SystemID int `json:"system_id"` Weight *float32 `json:"weight"` Score *float32 `json:"score"` @@ -44,7 +44,7 @@ func (q *Queries) AddAlert(ctx context.Context, arg AddAlertParams) error { _, err := q.db.Exec(ctx, addAlert, arg.ID, arg.Time, - arg.Tgid, + arg.TGID, arg.SystemID, arg.Weight, arg.Score, diff --git a/pkg/database/db.go b/pkg/database/db.go index 1d02744..8187a2b 100644 --- a/pkg/database/db.go +++ b/pkg/database/db.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.26.0 +// sqlc v1.27.0 package database diff --git a/pkg/database/models.go b/pkg/database/models.go index 8bb2b05..d40c03c 100644 --- a/pkg/database/models.go +++ b/pkg/database/models.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.26.0 +// sqlc v1.27.0 package database @@ -16,7 +16,7 @@ import ( type Alert struct { ID uuid.UUID `json:"id"` Time pgtype.Timestamptz `json:"time"` - Tgid int `json:"tgid"` + TGID int `json:"tgid"` SystemID int `json:"system_id"` Weight *float32 `json:"weight"` Score *float32 `json:"score"` @@ -85,7 +85,7 @@ type System struct { type Talkgroup struct { ID uuid.UUID `json:"id"` SystemID int32 `json:"system_id"` - Tgid int32 `json:"tgid"` + TGID int32 `json:"tgid"` Name *string `json:"name"` AlphaTag *string `json:"alpha_tag"` TgGroup *string `json:"tg_group"` @@ -100,7 +100,7 @@ type Talkgroup struct { type TalkgroupsLearned struct { ID int32 `json:"id"` SystemID int `json:"system_id"` - Tgid int `json:"tgid"` + TGID int `json:"tgid"` Name string `json:"name"` AlphaTag *string `json:"alpha_tag"` Ignored *bool `json:"ignored"` diff --git a/pkg/database/querier.go b/pkg/database/querier.go index 0f72f67..cd63b24 100644 --- a/pkg/database/querier.go +++ b/pkg/database/querier.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.26.0 +// sqlc v1.27.0 package database @@ -22,9 +22,9 @@ type Querier interface { GetDatabaseSize(ctx context.Context) (string, error) GetSystemName(ctx context.Context, systemID int) (string, error) GetTalkgroup(ctx context.Context, systemID int32, tgID int32) (GetTalkgroupRow, error) - GetTalkgroupIDsByTags(ctx context.Context, anytags []string, alltags []string, nottags []string) ([]GetTalkgroupIDsByTagsRow, error) + GetTalkgroupIDsByTags(ctx context.Context, anyTags []string, allTags []string, notTags []string) ([]GetTalkgroupIDsByTagsRow, error) GetTalkgroupTags(ctx context.Context, systemID int32, tgID int32) ([]string, error) - GetTalkgroupWithLearned(ctx context.Context, systemID int32, tgid int32) (GetTalkgroupWithLearnedRow, error) + GetTalkgroupWithLearned(ctx context.Context, systemID int32, tGID int32) (GetTalkgroupWithLearnedRow, error) GetTalkgroupsWithAllTags(ctx context.Context, tags []string) ([]GetTalkgroupsWithAllTagsRow, error) GetTalkgroupsWithAnyTags(ctx context.Context, tags []string) ([]GetTalkgroupsWithAnyTagsRow, error) GetTalkgroupsWithLearned(ctx context.Context) ([]GetTalkgroupsWithLearnedRow, error) diff --git a/pkg/database/talkgroups.go b/pkg/database/talkgroups.go index 0870892..f72cd47 100644 --- a/pkg/database/talkgroups.go +++ b/pkg/database/talkgroups.go @@ -55,7 +55,7 @@ func (q *Queries) GetTalkgroupsWithLearnedBySysTGID(ctx context.Context, ids TGT if err := rows.Scan( &i.Talkgroup.ID, &i.Talkgroup.SystemID, - &i.Talkgroup.Tgid, + &i.Talkgroup.TGID, &i.Talkgroup.Name, &i.Talkgroup.AlphaTag, &i.Talkgroup.TgGroup, @@ -95,7 +95,7 @@ func (q *Queries) GetTalkgroupsBySysTGID(ctx context.Context, ids TGTuples) ([]G if err := rows.Scan( &i.Talkgroup.ID, &i.Talkgroup.SystemID, - &i.Talkgroup.Tgid, + &i.Talkgroup.TGID, &i.Talkgroup.Name, &i.Talkgroup.AlphaTag, &i.Talkgroup.TgGroup, diff --git a/pkg/database/talkgroups.sql.go b/pkg/database/talkgroups.sql.go index a3836f9..46e8939 100644 --- a/pkg/database/talkgroups.sql.go +++ b/pkg/database/talkgroups.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.26.0 +// sqlc v1.27.0 // source: talkgroups.sql package database @@ -39,7 +39,7 @@ func (q *Queries) GetTalkgroup(ctx context.Context, systemID int32, tgID int32) err := row.Scan( &i.Talkgroup.ID, &i.Talkgroup.SystemID, - &i.Talkgroup.Tgid, + &i.Talkgroup.TGID, &i.Talkgroup.Name, &i.Talkgroup.AlphaTag, &i.Talkgroup.TgGroup, @@ -62,11 +62,11 @@ AND NOT (tags @> ARRAY[$3]) type GetTalkgroupIDsByTagsRow struct { SystemID int32 `json:"system_id"` - Tgid int32 `json:"tgid"` + TGID int32 `json:"tgid"` } -func (q *Queries) GetTalkgroupIDsByTags(ctx context.Context, anytags []string, alltags []string, nottags []string) ([]GetTalkgroupIDsByTagsRow, error) { - rows, err := q.db.Query(ctx, getTalkgroupIDsByTags, anytags, alltags, nottags) +func (q *Queries) GetTalkgroupIDsByTags(ctx context.Context, anyTags []string, allTags []string, notTags []string) ([]GetTalkgroupIDsByTagsRow, error) { + rows, err := q.db.Query(ctx, getTalkgroupIDsByTags, anyTags, allTags, notTags) if err != nil { return nil, err } @@ -74,7 +74,7 @@ func (q *Queries) GetTalkgroupIDsByTags(ctx context.Context, anytags []string, a var items []GetTalkgroupIDsByTagsRow for rows.Next() { var i GetTalkgroupIDsByTagsRow - if err := rows.Scan(&i.SystemID, &i.Tgid); err != nil { + if err := rows.Scan(&i.SystemID, &i.TGID); err != nil { return nil, err } items = append(items, i) @@ -122,13 +122,13 @@ type GetTalkgroupWithLearnedRow struct { Learned bool `json:"learned"` } -func (q *Queries) GetTalkgroupWithLearned(ctx context.Context, systemID int32, tgid int32) (GetTalkgroupWithLearnedRow, error) { - row := q.db.QueryRow(ctx, getTalkgroupWithLearned, systemID, tgid) +func (q *Queries) GetTalkgroupWithLearned(ctx context.Context, systemID int32, tGID int32) (GetTalkgroupWithLearnedRow, error) { + row := q.db.QueryRow(ctx, getTalkgroupWithLearned, systemID, tGID) var i GetTalkgroupWithLearnedRow err := row.Scan( &i.Talkgroup.ID, &i.Talkgroup.SystemID, - &i.Talkgroup.Tgid, + &i.Talkgroup.TGID, &i.Talkgroup.Name, &i.Talkgroup.AlphaTag, &i.Talkgroup.TgGroup, @@ -166,7 +166,7 @@ func (q *Queries) GetTalkgroupsWithAllTags(ctx context.Context, tags []string) ( if err := rows.Scan( &i.Talkgroup.ID, &i.Talkgroup.SystemID, - &i.Talkgroup.Tgid, + &i.Talkgroup.TGID, &i.Talkgroup.Name, &i.Talkgroup.AlphaTag, &i.Talkgroup.TgGroup, @@ -208,7 +208,7 @@ func (q *Queries) GetTalkgroupsWithAnyTags(ctx context.Context, tags []string) ( if err := rows.Scan( &i.Talkgroup.ID, &i.Talkgroup.SystemID, - &i.Talkgroup.Tgid, + &i.Talkgroup.TGID, &i.Talkgroup.Name, &i.Talkgroup.AlphaTag, &i.Talkgroup.TgGroup, @@ -265,7 +265,7 @@ func (q *Queries) GetTalkgroupsWithLearned(ctx context.Context) ([]GetTalkgroups if err := rows.Scan( &i.Talkgroup.ID, &i.Talkgroup.SystemID, - &i.Talkgroup.Tgid, + &i.Talkgroup.TGID, &i.Talkgroup.Name, &i.Talkgroup.AlphaTag, &i.Talkgroup.TgGroup, @@ -326,7 +326,7 @@ func (q *Queries) GetTalkgroupsWithLearnedBySystem(ctx context.Context, system i if err := rows.Scan( &i.Talkgroup.ID, &i.Talkgroup.SystemID, - &i.Talkgroup.Tgid, + &i.Talkgroup.TGID, &i.Talkgroup.Name, &i.Talkgroup.AlphaTag, &i.Talkgroup.TgGroup, @@ -388,7 +388,7 @@ type UpdateTalkgroupParams struct { Weight *float32 `json:"weight"` ID pgtype.UUID `json:"id"` SystemID *int32 `json:"system_id"` - Tgid *int32 `json:"tgid"` + TGID *int32 `json:"tgid"` } func (q *Queries) UpdateTalkgroup(ctx context.Context, arg UpdateTalkgroupParams) (Talkgroup, error) { @@ -404,13 +404,13 @@ func (q *Queries) UpdateTalkgroup(ctx context.Context, arg UpdateTalkgroupParams arg.Weight, arg.ID, arg.SystemID, - arg.Tgid, + arg.TGID, ) var i Talkgroup err := row.Scan( &i.ID, &i.SystemID, - &i.Tgid, + &i.TGID, &i.Name, &i.AlphaTag, &i.TgGroup, diff --git a/pkg/database/users.sql.go b/pkg/database/users.sql.go index ea9eb0a..479cbc8 100644 --- a/pkg/database/users.sql.go +++ b/pkg/database/users.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.26.0 +// sqlc v1.27.0 // source: users.sql package database diff --git a/pkg/talkgroups/cache.go b/pkg/talkgroups/cache.go index 84bc070..9d0184b 100644 --- a/pkg/talkgroups/cache.go +++ b/pkg/talkgroups/cache.go @@ -128,7 +128,7 @@ func (t *cache) add(rec *Talkgroup) error { t.Lock() defer t.Unlock() - tg := TG(rec.System.ID, rec.Talkgroup.Tgid) + tg := TG(rec.System.ID, rec.Talkgroup.TGID) t.tgs[tg] = rec t.systems[int32(rec.System.ID)] = rec.System.Name diff --git a/pkg/talkgroups/importer/import.go b/pkg/talkgroups/importer/import.go index 667936d..5d0493a 100644 --- a/pkg/talkgroups/importer/import.go +++ b/pkg/talkgroups/importer/import.go @@ -120,7 +120,7 @@ func (rr *radioReferenceImporter) importTalkgroups(ctx context.Context, sys int, tgs = append(tgs, talkgroups.Talkgroup{ Talkgroup: database.Talkgroup{ ID: uuid.New(), - Tgid: int32(tgt.Talkgroup), + TGID: int32(tgt.Talkgroup), SystemID: int32(tgt.System), Name: &fields[4], AlphaTag: &fields[3], diff --git a/sql/postgres/queries/talkgroups.sql b/sql/postgres/queries/talkgroups.sql index 894f4d8..7ad3e53 100644 --- a/sql/postgres/queries/talkgroups.sql +++ b/sql/postgres/queries/talkgroups.sql @@ -8,9 +8,9 @@ WHERE tags && ARRAY[$1]; -- name: GetTalkgroupIDsByTags :many SELECT system_id, tgid FROM talkgroups -WHERE (tags @> ARRAY[@anyTags]) -AND (tags && ARRAY[@allTags]) -AND NOT (tags @> ARRAY[@notTags]); +WHERE (tags @> ARRAY[@any_tags]) +AND (tags && ARRAY[@all_tags]) +AND NOT (tags @> ARRAY[@not_tags]); -- name: GetTalkgroupTags :one SELECT tags FROM talkgroups diff --git a/sql/sqlc.yaml b/sql/sqlc.yaml index 342e2fb..506170a 100644 --- a/sql/sqlc.yaml +++ b/sql/sqlc.yaml @@ -11,6 +11,9 @@ sql: query_parameter_limit: 3 emit_json_tags: true emit_interface: true + initialisms: + - id + - tgid emit_pointers_for_null_types: true overrides: - db_type: "uuid" -- 2.47.0 From c9a32cd4bfc5e45faa3c9b0383ef2f2353802548 Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Fri, 15 Nov 2024 12:18:32 -0500 Subject: [PATCH 16/17] pre-mock --- Makefile | 1 + go.mod | 1 + go.sum | 2 ++ pkg/alerting/alerting.go | 2 +- pkg/auth/jwt.go | 2 +- pkg/database/database.go | 24 ++++++++++++++++++------ pkg/database/talkgroups.go | 6 ++++++ pkg/server/server.go | 4 ++-- pkg/sinks/database.go | 4 ++-- pkg/talkgroups/importer/import_test.go | 5 ++++- 10 files changed, 38 insertions(+), 13 deletions(-) diff --git a/Makefile b/Makefile index 1c0af40..270b216 100644 --- a/Makefile +++ b/Makefile @@ -25,6 +25,7 @@ getcalls: generate: sqlc generate -f sql/sqlc.yaml protoc -I=pkg/pb/ --go_out=pkg/ pkg/pb/stillbox.proto + go generate ./... lint: golangci-lint run diff --git a/go.mod b/go.mod index 3fe31ca..6091dc0 100644 --- a/go.mod +++ b/go.mod @@ -57,6 +57,7 @@ require ( github.com/rogpeppe/go-internal v1.12.0 // indirect github.com/segmentio/asm v1.2.0 // indirect github.com/spf13/pflag v1.0.5 // indirect + github.com/stretchr/objx v0.5.2 // indirect go.uber.org/atomic v1.7.0 // indirect golang.org/x/exp/shiny v0.0.0-20240719175910-8a7402abbf56 // indirect golang.org/x/image v0.14.0 // indirect diff --git a/go.sum b/go.sum index 3ca4872..9e5b411 100644 --- a/go.sum +++ b/go.sum @@ -134,6 +134,8 @@ github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3k github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= diff --git a/pkg/alerting/alerting.go b/pkg/alerting/alerting.go index 316f266..5061fca 100644 --- a/pkg/alerting/alerting.go +++ b/pkg/alerting/alerting.go @@ -312,7 +312,7 @@ func (as *alerter) backfill(ctx context.Context, since time.Time, until time.Tim db := database.FromCtx(ctx) const backfillStatsQuery = `SELECT system, talkgroup, call_date FROM calls WHERE call_date > $1 AND call_date < $2 ORDER BY call_date ASC` - rows, err := db.Query(ctx, backfillStatsQuery, since, until) + rows, err := db.DB().Query(ctx, backfillStatsQuery, since, until) if err != nil { return count, err } diff --git a/pkg/auth/jwt.go b/pkg/auth/jwt.go index 6241805..d85bbb8 100644 --- a/pkg/auth/jwt.go +++ b/pkg/auth/jwt.go @@ -70,7 +70,7 @@ func (a *Auth) initJWT() { } func (a *Auth) Login(ctx context.Context, username, password string) (token string, err error) { - q := database.New(database.FromCtx(ctx)) + q := database.FromCtx(ctx) users, err := q.GetUsers(ctx) if err != nil { log.Error().Err(err).Msg("getUsers failed") diff --git a/pkg/database/database.go b/pkg/database/database.go index d1a7e06..5bd216b 100644 --- a/pkg/database/database.go +++ b/pkg/database/database.go @@ -16,11 +16,23 @@ import ( ) // DB is a database handle. -type DB struct { +//go:generate mockery +type DB interface { + Querier + talkgroupQuerier + + DB() *Database +} + +type Database struct { *pgxpool.Pool *Queries } +func (db *Database) DB() *Database { + return db +} + type dbLogger struct{} func (m dbLogger) Log(ctx context.Context, level tracelog.LogLevel, msg string, data map[string]any) { @@ -28,7 +40,7 @@ func (m dbLogger) Log(ctx context.Context, level tracelog.LogLevel, msg string, } // NewClient creates a new DB using the provided config. -func NewClient(ctx context.Context, conf config.DB) (*DB, error) { +func NewClient(ctx context.Context, conf config.DB) (DB, error) { dir, err := iofs.New(sqlembed.Migrations, "postgres/migrations") if err != nil { return nil, err @@ -63,7 +75,7 @@ func NewClient(ctx context.Context, conf config.DB) (*DB, error) { return nil, err } - db := &DB{ + db := &Database{ Pool: pool, Queries: New(pool), } @@ -76,8 +88,8 @@ type dBCtxKey string const DBCtxKey dBCtxKey = "dbctx" // FromCtx returns the database handle from the provided Context. -func FromCtx(ctx context.Context) *DB { - c, ok := ctx.Value(DBCtxKey).(*DB) +func FromCtx(ctx context.Context) DB { + c, ok := ctx.Value(DBCtxKey).(DB) if !ok { panic("no DB in context") } @@ -86,7 +98,7 @@ func FromCtx(ctx context.Context) *DB { } // CtxWithDB returns a Context with the provided database handle. -func CtxWithDB(ctx context.Context, conn *DB) context.Context { +func CtxWithDB(ctx context.Context, conn DB) context.Context { return context.WithValue(ctx, DBCtxKey, conn) } diff --git a/pkg/database/talkgroups.go b/pkg/database/talkgroups.go index f72cd47..40d3b2b 100644 --- a/pkg/database/talkgroups.go +++ b/pkg/database/talkgroups.go @@ -4,6 +4,12 @@ import ( "context" ) +type talkgroupQuerier interface { + GetTalkgroupsWithLearnedBySysTGID(ctx context.Context, ids TGTuples) ([]GetTalkgroupsRow, error) + GetTalkgroupsBySysTGID(ctx context.Context, ids TGTuples) ([]GetTalkgroupsRow, error) + BulkSetTalkgroupTags(ctx context.Context, tgs TGTuples, tags []string) error +} + type TGTuples [2][]uint32 func MakeTGTuples(cap int) TGTuples { diff --git a/pkg/server/server.go b/pkg/server/server.go index f527214..a5ff3bd 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -27,7 +27,7 @@ const shutdownTimeout = 5 * time.Second type Server struct { auth *auth.Auth conf *config.Config - db *database.DB + db database.DB r *chi.Mux sources sources.Sources sinks sinks.Sinks @@ -103,7 +103,7 @@ func New(ctx context.Context, cfg *config.Config) (*Server, error) { } func (s *Server) Go(ctx context.Context) error { - defer s.db.Close() + defer s.db.DB().Close() s.installHupHandler() diff --git a/pkg/sinks/database.go b/pkg/sinks/database.go index 22bf75c..fa557cc 100644 --- a/pkg/sinks/database.go +++ b/pkg/sinks/database.go @@ -13,10 +13,10 @@ import ( ) type DatabaseSink struct { - db *database.DB + db database.DB } -func NewDatabaseSink(db *database.DB) *DatabaseSink { +func NewDatabaseSink(db database.DB) *DatabaseSink { return &DatabaseSink{db: db} } diff --git a/pkg/talkgroups/importer/import_test.go b/pkg/talkgroups/importer/import_test.go index d281496..7ca74c2 100644 --- a/pkg/talkgroups/importer/import_test.go +++ b/pkg/talkgroups/importer/import_test.go @@ -10,6 +10,8 @@ import ( "github.com/stretchr/testify/require" "dynatron.me/x/stillbox/pkg/talkgroups/importer" + "dynatron.me/x/stillbox/pkg/database" + "dynatron.me/x/stillbox/pkg/database/mocks" ) func getFixture(fixture string) []byte { @@ -23,7 +25,6 @@ func getFixture(fixture string) []byte { } func TestRadioReferenceImport(t *testing.T) { - ctx := context.Background() tests := []struct{ name string input []byte @@ -41,6 +42,8 @@ func TestRadioReferenceImport(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { + ctx := context.Background() + ctx = database.CtxWithDB(ctx, mocks.NewDB()) ij := &importer.ImportJob{ Type: "radioreference", SystemID: tc.sysID, -- 2.47.0 From 1b051c6ad9b722cc71290202c267fd13d28d4269 Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Fri, 15 Nov 2024 13:06:37 -0500 Subject: [PATCH 17/17] Importer test --- .mockery.yaml | 10 + pkg/database/database.go | 1 + pkg/database/mocks/DB.go | 1631 ++++++++++++++++++ pkg/talkgroups/importer/import.go | 11 +- pkg/talkgroups/importer/import_test.go | 57 +- pkg/talkgroups/importer/testdata/riscon.json | 1 + 6 files changed, 1684 insertions(+), 27 deletions(-) create mode 100644 .mockery.yaml create mode 100644 pkg/database/mocks/DB.go diff --git a/.mockery.yaml b/.mockery.yaml new file mode 100644 index 0000000..ab4e084 --- /dev/null +++ b/.mockery.yaml @@ -0,0 +1,10 @@ +dir: '{{ replaceAll .InterfaceDirRelative "internal" "internal_" }}/mocks' +mockname: "{{.InterfaceName}}" +outpkg: "mocks" +filename: "{{.InterfaceName}}.go" +with-expecter: true +packages: + dynatron.me/x/stillbox/pkg/database: + config: + interfaces: + DB: diff --git a/pkg/database/database.go b/pkg/database/database.go index 5bd216b..1925545 100644 --- a/pkg/database/database.go +++ b/pkg/database/database.go @@ -16,6 +16,7 @@ import ( ) // DB is a database handle. + //go:generate mockery type DB interface { Querier diff --git a/pkg/database/mocks/DB.go b/pkg/database/mocks/DB.go new file mode 100644 index 0000000..f7b451d --- /dev/null +++ b/pkg/database/mocks/DB.go @@ -0,0 +1,1631 @@ +// Code generated by mockery v2.47.0. DO NOT EDIT. + +package mocks + +import ( + context "context" + + database "dynatron.me/x/stillbox/pkg/database" + mock "github.com/stretchr/testify/mock" + + pgtype "github.com/jackc/pgx/v5/pgtype" + + uuid "github.com/google/uuid" +) + +// DB is an autogenerated mock type for the DB type +type DB struct { + mock.Mock +} + +type DB_Expecter struct { + mock *mock.Mock +} + +func (_m *DB) EXPECT() *DB_Expecter { + return &DB_Expecter{mock: &_m.Mock} +} + +// AddAlert provides a mock function with given fields: ctx, arg +func (_m *DB) AddAlert(ctx context.Context, arg database.AddAlertParams) error { + ret := _m.Called(ctx, arg) + + if len(ret) == 0 { + panic("no return value specified for AddAlert") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, database.AddAlertParams) error); ok { + r0 = rf(ctx, arg) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// DB_AddAlert_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddAlert' +type DB_AddAlert_Call struct { + *mock.Call +} + +// AddAlert is a helper method to define mock.On call +// - ctx context.Context +// - arg database.AddAlertParams +func (_e *DB_Expecter) AddAlert(ctx interface{}, arg interface{}) *DB_AddAlert_Call { + return &DB_AddAlert_Call{Call: _e.mock.On("AddAlert", ctx, arg)} +} + +func (_c *DB_AddAlert_Call) Run(run func(ctx context.Context, arg database.AddAlertParams)) *DB_AddAlert_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(database.AddAlertParams)) + }) + return _c +} + +func (_c *DB_AddAlert_Call) Return(_a0 error) *DB_AddAlert_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *DB_AddAlert_Call) RunAndReturn(run func(context.Context, database.AddAlertParams) error) *DB_AddAlert_Call { + _c.Call.Return(run) + return _c +} + +// AddCall provides a mock function with given fields: ctx, arg +func (_m *DB) AddCall(ctx context.Context, arg database.AddCallParams) error { + ret := _m.Called(ctx, arg) + + if len(ret) == 0 { + panic("no return value specified for AddCall") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, database.AddCallParams) error); ok { + r0 = rf(ctx, arg) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// DB_AddCall_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddCall' +type DB_AddCall_Call struct { + *mock.Call +} + +// AddCall is a helper method to define mock.On call +// - ctx context.Context +// - arg database.AddCallParams +func (_e *DB_Expecter) AddCall(ctx interface{}, arg interface{}) *DB_AddCall_Call { + return &DB_AddCall_Call{Call: _e.mock.On("AddCall", ctx, arg)} +} + +func (_c *DB_AddCall_Call) Run(run func(ctx context.Context, arg database.AddCallParams)) *DB_AddCall_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(database.AddCallParams)) + }) + return _c +} + +func (_c *DB_AddCall_Call) Return(_a0 error) *DB_AddCall_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *DB_AddCall_Call) RunAndReturn(run func(context.Context, database.AddCallParams) error) *DB_AddCall_Call { + _c.Call.Return(run) + return _c +} + +// BulkSetTalkgroupTags provides a mock function with given fields: ctx, tgs, tags +func (_m *DB) BulkSetTalkgroupTags(ctx context.Context, tgs database.TGTuples, tags []string) error { + ret := _m.Called(ctx, tgs, tags) + + if len(ret) == 0 { + panic("no return value specified for BulkSetTalkgroupTags") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, database.TGTuples, []string) error); ok { + r0 = rf(ctx, tgs, tags) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// DB_BulkSetTalkgroupTags_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'BulkSetTalkgroupTags' +type DB_BulkSetTalkgroupTags_Call struct { + *mock.Call +} + +// BulkSetTalkgroupTags is a helper method to define mock.On call +// - ctx context.Context +// - tgs database.TGTuples +// - tags []string +func (_e *DB_Expecter) BulkSetTalkgroupTags(ctx interface{}, tgs interface{}, tags interface{}) *DB_BulkSetTalkgroupTags_Call { + return &DB_BulkSetTalkgroupTags_Call{Call: _e.mock.On("BulkSetTalkgroupTags", ctx, tgs, tags)} +} + +func (_c *DB_BulkSetTalkgroupTags_Call) Run(run func(ctx context.Context, tgs database.TGTuples, tags []string)) *DB_BulkSetTalkgroupTags_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(database.TGTuples), args[2].([]string)) + }) + return _c +} + +func (_c *DB_BulkSetTalkgroupTags_Call) Return(_a0 error) *DB_BulkSetTalkgroupTags_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *DB_BulkSetTalkgroupTags_Call) RunAndReturn(run func(context.Context, database.TGTuples, []string) error) *DB_BulkSetTalkgroupTags_Call { + _c.Call.Return(run) + return _c +} + +// CreateAPIKey provides a mock function with given fields: ctx, owner, expires, disabled +func (_m *DB) CreateAPIKey(ctx context.Context, owner int, expires pgtype.Timestamp, disabled *bool) (database.ApiKey, error) { + ret := _m.Called(ctx, owner, expires, disabled) + + if len(ret) == 0 { + panic("no return value specified for CreateAPIKey") + } + + var r0 database.ApiKey + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, int, pgtype.Timestamp, *bool) (database.ApiKey, error)); ok { + return rf(ctx, owner, expires, disabled) + } + if rf, ok := ret.Get(0).(func(context.Context, int, pgtype.Timestamp, *bool) database.ApiKey); ok { + r0 = rf(ctx, owner, expires, disabled) + } else { + r0 = ret.Get(0).(database.ApiKey) + } + + if rf, ok := ret.Get(1).(func(context.Context, int, pgtype.Timestamp, *bool) error); ok { + r1 = rf(ctx, owner, expires, disabled) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// DB_CreateAPIKey_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateAPIKey' +type DB_CreateAPIKey_Call struct { + *mock.Call +} + +// CreateAPIKey is a helper method to define mock.On call +// - ctx context.Context +// - owner int +// - expires pgtype.Timestamp +// - disabled *bool +func (_e *DB_Expecter) CreateAPIKey(ctx interface{}, owner interface{}, expires interface{}, disabled interface{}) *DB_CreateAPIKey_Call { + return &DB_CreateAPIKey_Call{Call: _e.mock.On("CreateAPIKey", ctx, owner, expires, disabled)} +} + +func (_c *DB_CreateAPIKey_Call) Run(run func(ctx context.Context, owner int, expires pgtype.Timestamp, disabled *bool)) *DB_CreateAPIKey_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(int), args[2].(pgtype.Timestamp), args[3].(*bool)) + }) + return _c +} + +func (_c *DB_CreateAPIKey_Call) Return(_a0 database.ApiKey, _a1 error) *DB_CreateAPIKey_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *DB_CreateAPIKey_Call) RunAndReturn(run func(context.Context, int, pgtype.Timestamp, *bool) (database.ApiKey, error)) *DB_CreateAPIKey_Call { + _c.Call.Return(run) + return _c +} + +// CreateUser provides a mock function with given fields: ctx, arg +func (_m *DB) CreateUser(ctx context.Context, arg database.CreateUserParams) (database.User, error) { + ret := _m.Called(ctx, arg) + + if len(ret) == 0 { + panic("no return value specified for CreateUser") + } + + var r0 database.User + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, database.CreateUserParams) (database.User, error)); ok { + return rf(ctx, arg) + } + if rf, ok := ret.Get(0).(func(context.Context, database.CreateUserParams) database.User); ok { + r0 = rf(ctx, arg) + } else { + r0 = ret.Get(0).(database.User) + } + + if rf, ok := ret.Get(1).(func(context.Context, database.CreateUserParams) error); ok { + r1 = rf(ctx, arg) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// DB_CreateUser_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateUser' +type DB_CreateUser_Call struct { + *mock.Call +} + +// CreateUser is a helper method to define mock.On call +// - ctx context.Context +// - arg database.CreateUserParams +func (_e *DB_Expecter) CreateUser(ctx interface{}, arg interface{}) *DB_CreateUser_Call { + return &DB_CreateUser_Call{Call: _e.mock.On("CreateUser", ctx, arg)} +} + +func (_c *DB_CreateUser_Call) Run(run func(ctx context.Context, arg database.CreateUserParams)) *DB_CreateUser_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(database.CreateUserParams)) + }) + return _c +} + +func (_c *DB_CreateUser_Call) Return(_a0 database.User, _a1 error) *DB_CreateUser_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *DB_CreateUser_Call) RunAndReturn(run func(context.Context, database.CreateUserParams) (database.User, error)) *DB_CreateUser_Call { + _c.Call.Return(run) + return _c +} + +// DB provides a mock function with given fields: +func (_m *DB) DB() *database.Database { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for DB") + } + + var r0 *database.Database + if rf, ok := ret.Get(0).(func() *database.Database); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*database.Database) + } + } + + return r0 +} + +// DB_DB_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DB' +type DB_DB_Call struct { + *mock.Call +} + +// DB is a helper method to define mock.On call +func (_e *DB_Expecter) DB() *DB_DB_Call { + return &DB_DB_Call{Call: _e.mock.On("DB")} +} + +func (_c *DB_DB_Call) Run(run func()) *DB_DB_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *DB_DB_Call) Return(_a0 *database.Database) *DB_DB_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *DB_DB_Call) RunAndReturn(run func() *database.Database) *DB_DB_Call { + _c.Call.Return(run) + return _c +} + +// DeleteAPIKey provides a mock function with given fields: ctx, apiKey +func (_m *DB) DeleteAPIKey(ctx context.Context, apiKey string) error { + ret := _m.Called(ctx, apiKey) + + if len(ret) == 0 { + panic("no return value specified for DeleteAPIKey") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { + r0 = rf(ctx, apiKey) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// DB_DeleteAPIKey_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteAPIKey' +type DB_DeleteAPIKey_Call struct { + *mock.Call +} + +// DeleteAPIKey is a helper method to define mock.On call +// - ctx context.Context +// - apiKey string +func (_e *DB_Expecter) DeleteAPIKey(ctx interface{}, apiKey interface{}) *DB_DeleteAPIKey_Call { + return &DB_DeleteAPIKey_Call{Call: _e.mock.On("DeleteAPIKey", ctx, apiKey)} +} + +func (_c *DB_DeleteAPIKey_Call) Run(run func(ctx context.Context, apiKey string)) *DB_DeleteAPIKey_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string)) + }) + return _c +} + +func (_c *DB_DeleteAPIKey_Call) Return(_a0 error) *DB_DeleteAPIKey_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *DB_DeleteAPIKey_Call) RunAndReturn(run func(context.Context, string) error) *DB_DeleteAPIKey_Call { + _c.Call.Return(run) + return _c +} + +// DeleteUser provides a mock function with given fields: ctx, username +func (_m *DB) DeleteUser(ctx context.Context, username string) error { + ret := _m.Called(ctx, username) + + if len(ret) == 0 { + panic("no return value specified for DeleteUser") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { + r0 = rf(ctx, username) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// DB_DeleteUser_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteUser' +type DB_DeleteUser_Call struct { + *mock.Call +} + +// DeleteUser is a helper method to define mock.On call +// - ctx context.Context +// - username string +func (_e *DB_Expecter) DeleteUser(ctx interface{}, username interface{}) *DB_DeleteUser_Call { + return &DB_DeleteUser_Call{Call: _e.mock.On("DeleteUser", ctx, username)} +} + +func (_c *DB_DeleteUser_Call) Run(run func(ctx context.Context, username string)) *DB_DeleteUser_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string)) + }) + return _c +} + +func (_c *DB_DeleteUser_Call) Return(_a0 error) *DB_DeleteUser_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *DB_DeleteUser_Call) RunAndReturn(run func(context.Context, string) error) *DB_DeleteUser_Call { + _c.Call.Return(run) + return _c +} + +// GetAPIKey provides a mock function with given fields: ctx, apiKey +func (_m *DB) GetAPIKey(ctx context.Context, apiKey string) (database.ApiKey, error) { + ret := _m.Called(ctx, apiKey) + + if len(ret) == 0 { + panic("no return value specified for GetAPIKey") + } + + var r0 database.ApiKey + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (database.ApiKey, error)); ok { + return rf(ctx, apiKey) + } + if rf, ok := ret.Get(0).(func(context.Context, string) database.ApiKey); ok { + r0 = rf(ctx, apiKey) + } else { + r0 = ret.Get(0).(database.ApiKey) + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, apiKey) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// DB_GetAPIKey_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetAPIKey' +type DB_GetAPIKey_Call struct { + *mock.Call +} + +// GetAPIKey is a helper method to define mock.On call +// - ctx context.Context +// - apiKey string +func (_e *DB_Expecter) GetAPIKey(ctx interface{}, apiKey interface{}) *DB_GetAPIKey_Call { + return &DB_GetAPIKey_Call{Call: _e.mock.On("GetAPIKey", ctx, apiKey)} +} + +func (_c *DB_GetAPIKey_Call) Run(run func(ctx context.Context, apiKey string)) *DB_GetAPIKey_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string)) + }) + return _c +} + +func (_c *DB_GetAPIKey_Call) Return(_a0 database.ApiKey, _a1 error) *DB_GetAPIKey_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *DB_GetAPIKey_Call) RunAndReturn(run func(context.Context, string) (database.ApiKey, error)) *DB_GetAPIKey_Call { + _c.Call.Return(run) + return _c +} + +// GetDatabaseSize provides a mock function with given fields: ctx +func (_m *DB) GetDatabaseSize(ctx context.Context) (string, error) { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for GetDatabaseSize") + } + + var r0 string + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) (string, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) string); ok { + r0 = rf(ctx) + } else { + r0 = ret.Get(0).(string) + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// DB_GetDatabaseSize_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetDatabaseSize' +type DB_GetDatabaseSize_Call struct { + *mock.Call +} + +// GetDatabaseSize is a helper method to define mock.On call +// - ctx context.Context +func (_e *DB_Expecter) GetDatabaseSize(ctx interface{}) *DB_GetDatabaseSize_Call { + return &DB_GetDatabaseSize_Call{Call: _e.mock.On("GetDatabaseSize", ctx)} +} + +func (_c *DB_GetDatabaseSize_Call) Run(run func(ctx context.Context)) *DB_GetDatabaseSize_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *DB_GetDatabaseSize_Call) Return(_a0 string, _a1 error) *DB_GetDatabaseSize_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *DB_GetDatabaseSize_Call) RunAndReturn(run func(context.Context) (string, error)) *DB_GetDatabaseSize_Call { + _c.Call.Return(run) + return _c +} + +// GetSystemName provides a mock function with given fields: ctx, systemID +func (_m *DB) GetSystemName(ctx context.Context, systemID int) (string, error) { + ret := _m.Called(ctx, systemID) + + if len(ret) == 0 { + panic("no return value specified for GetSystemName") + } + + var r0 string + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, int) (string, error)); ok { + return rf(ctx, systemID) + } + if rf, ok := ret.Get(0).(func(context.Context, int) string); ok { + r0 = rf(ctx, systemID) + } else { + r0 = ret.Get(0).(string) + } + + if rf, ok := ret.Get(1).(func(context.Context, int) error); ok { + r1 = rf(ctx, systemID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// DB_GetSystemName_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetSystemName' +type DB_GetSystemName_Call struct { + *mock.Call +} + +// GetSystemName is a helper method to define mock.On call +// - ctx context.Context +// - systemID int +func (_e *DB_Expecter) GetSystemName(ctx interface{}, systemID interface{}) *DB_GetSystemName_Call { + return &DB_GetSystemName_Call{Call: _e.mock.On("GetSystemName", ctx, systemID)} +} + +func (_c *DB_GetSystemName_Call) Run(run func(ctx context.Context, systemID int)) *DB_GetSystemName_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(int)) + }) + return _c +} + +func (_c *DB_GetSystemName_Call) Return(_a0 string, _a1 error) *DB_GetSystemName_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *DB_GetSystemName_Call) RunAndReturn(run func(context.Context, int) (string, error)) *DB_GetSystemName_Call { + _c.Call.Return(run) + return _c +} + +// GetTalkgroup provides a mock function with given fields: ctx, systemID, tgID +func (_m *DB) GetTalkgroup(ctx context.Context, systemID int32, tgID int32) (database.GetTalkgroupRow, error) { + ret := _m.Called(ctx, systemID, tgID) + + if len(ret) == 0 { + panic("no return value specified for GetTalkgroup") + } + + var r0 database.GetTalkgroupRow + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, int32, int32) (database.GetTalkgroupRow, error)); ok { + return rf(ctx, systemID, tgID) + } + if rf, ok := ret.Get(0).(func(context.Context, int32, int32) database.GetTalkgroupRow); ok { + r0 = rf(ctx, systemID, tgID) + } else { + r0 = ret.Get(0).(database.GetTalkgroupRow) + } + + if rf, ok := ret.Get(1).(func(context.Context, int32, int32) error); ok { + r1 = rf(ctx, systemID, tgID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// DB_GetTalkgroup_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetTalkgroup' +type DB_GetTalkgroup_Call struct { + *mock.Call +} + +// GetTalkgroup is a helper method to define mock.On call +// - ctx context.Context +// - systemID int32 +// - tgID int32 +func (_e *DB_Expecter) GetTalkgroup(ctx interface{}, systemID interface{}, tgID interface{}) *DB_GetTalkgroup_Call { + return &DB_GetTalkgroup_Call{Call: _e.mock.On("GetTalkgroup", ctx, systemID, tgID)} +} + +func (_c *DB_GetTalkgroup_Call) Run(run func(ctx context.Context, systemID int32, tgID int32)) *DB_GetTalkgroup_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(int32), args[2].(int32)) + }) + return _c +} + +func (_c *DB_GetTalkgroup_Call) Return(_a0 database.GetTalkgroupRow, _a1 error) *DB_GetTalkgroup_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *DB_GetTalkgroup_Call) RunAndReturn(run func(context.Context, int32, int32) (database.GetTalkgroupRow, error)) *DB_GetTalkgroup_Call { + _c.Call.Return(run) + return _c +} + +// GetTalkgroupIDsByTags provides a mock function with given fields: ctx, anyTags, allTags, notTags +func (_m *DB) GetTalkgroupIDsByTags(ctx context.Context, anyTags []string, allTags []string, notTags []string) ([]database.GetTalkgroupIDsByTagsRow, error) { + ret := _m.Called(ctx, anyTags, allTags, notTags) + + if len(ret) == 0 { + panic("no return value specified for GetTalkgroupIDsByTags") + } + + var r0 []database.GetTalkgroupIDsByTagsRow + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, []string, []string, []string) ([]database.GetTalkgroupIDsByTagsRow, error)); ok { + return rf(ctx, anyTags, allTags, notTags) + } + if rf, ok := ret.Get(0).(func(context.Context, []string, []string, []string) []database.GetTalkgroupIDsByTagsRow); ok { + r0 = rf(ctx, anyTags, allTags, notTags) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]database.GetTalkgroupIDsByTagsRow) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, []string, []string, []string) error); ok { + r1 = rf(ctx, anyTags, allTags, notTags) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// DB_GetTalkgroupIDsByTags_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetTalkgroupIDsByTags' +type DB_GetTalkgroupIDsByTags_Call struct { + *mock.Call +} + +// GetTalkgroupIDsByTags is a helper method to define mock.On call +// - ctx context.Context +// - anyTags []string +// - allTags []string +// - notTags []string +func (_e *DB_Expecter) GetTalkgroupIDsByTags(ctx interface{}, anyTags interface{}, allTags interface{}, notTags interface{}) *DB_GetTalkgroupIDsByTags_Call { + return &DB_GetTalkgroupIDsByTags_Call{Call: _e.mock.On("GetTalkgroupIDsByTags", ctx, anyTags, allTags, notTags)} +} + +func (_c *DB_GetTalkgroupIDsByTags_Call) Run(run func(ctx context.Context, anyTags []string, allTags []string, notTags []string)) *DB_GetTalkgroupIDsByTags_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].([]string), args[2].([]string), args[3].([]string)) + }) + return _c +} + +func (_c *DB_GetTalkgroupIDsByTags_Call) Return(_a0 []database.GetTalkgroupIDsByTagsRow, _a1 error) *DB_GetTalkgroupIDsByTags_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *DB_GetTalkgroupIDsByTags_Call) RunAndReturn(run func(context.Context, []string, []string, []string) ([]database.GetTalkgroupIDsByTagsRow, error)) *DB_GetTalkgroupIDsByTags_Call { + _c.Call.Return(run) + return _c +} + +// GetTalkgroupTags provides a mock function with given fields: ctx, systemID, tgID +func (_m *DB) GetTalkgroupTags(ctx context.Context, systemID int32, tgID int32) ([]string, error) { + ret := _m.Called(ctx, systemID, tgID) + + if len(ret) == 0 { + panic("no return value specified for GetTalkgroupTags") + } + + var r0 []string + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, int32, int32) ([]string, error)); ok { + return rf(ctx, systemID, tgID) + } + if rf, ok := ret.Get(0).(func(context.Context, int32, int32) []string); ok { + r0 = rf(ctx, systemID, tgID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]string) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, int32, int32) error); ok { + r1 = rf(ctx, systemID, tgID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// DB_GetTalkgroupTags_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetTalkgroupTags' +type DB_GetTalkgroupTags_Call struct { + *mock.Call +} + +// GetTalkgroupTags is a helper method to define mock.On call +// - ctx context.Context +// - systemID int32 +// - tgID int32 +func (_e *DB_Expecter) GetTalkgroupTags(ctx interface{}, systemID interface{}, tgID interface{}) *DB_GetTalkgroupTags_Call { + return &DB_GetTalkgroupTags_Call{Call: _e.mock.On("GetTalkgroupTags", ctx, systemID, tgID)} +} + +func (_c *DB_GetTalkgroupTags_Call) Run(run func(ctx context.Context, systemID int32, tgID int32)) *DB_GetTalkgroupTags_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(int32), args[2].(int32)) + }) + return _c +} + +func (_c *DB_GetTalkgroupTags_Call) Return(_a0 []string, _a1 error) *DB_GetTalkgroupTags_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *DB_GetTalkgroupTags_Call) RunAndReturn(run func(context.Context, int32, int32) ([]string, error)) *DB_GetTalkgroupTags_Call { + _c.Call.Return(run) + return _c +} + +// GetTalkgroupWithLearned provides a mock function with given fields: ctx, systemID, tGID +func (_m *DB) GetTalkgroupWithLearned(ctx context.Context, systemID int32, tGID int32) (database.GetTalkgroupWithLearnedRow, error) { + ret := _m.Called(ctx, systemID, tGID) + + if len(ret) == 0 { + panic("no return value specified for GetTalkgroupWithLearned") + } + + var r0 database.GetTalkgroupWithLearnedRow + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, int32, int32) (database.GetTalkgroupWithLearnedRow, error)); ok { + return rf(ctx, systemID, tGID) + } + if rf, ok := ret.Get(0).(func(context.Context, int32, int32) database.GetTalkgroupWithLearnedRow); ok { + r0 = rf(ctx, systemID, tGID) + } else { + r0 = ret.Get(0).(database.GetTalkgroupWithLearnedRow) + } + + if rf, ok := ret.Get(1).(func(context.Context, int32, int32) error); ok { + r1 = rf(ctx, systemID, tGID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// DB_GetTalkgroupWithLearned_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetTalkgroupWithLearned' +type DB_GetTalkgroupWithLearned_Call struct { + *mock.Call +} + +// GetTalkgroupWithLearned is a helper method to define mock.On call +// - ctx context.Context +// - systemID int32 +// - tGID int32 +func (_e *DB_Expecter) GetTalkgroupWithLearned(ctx interface{}, systemID interface{}, tGID interface{}) *DB_GetTalkgroupWithLearned_Call { + return &DB_GetTalkgroupWithLearned_Call{Call: _e.mock.On("GetTalkgroupWithLearned", ctx, systemID, tGID)} +} + +func (_c *DB_GetTalkgroupWithLearned_Call) Run(run func(ctx context.Context, systemID int32, tGID int32)) *DB_GetTalkgroupWithLearned_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(int32), args[2].(int32)) + }) + return _c +} + +func (_c *DB_GetTalkgroupWithLearned_Call) Return(_a0 database.GetTalkgroupWithLearnedRow, _a1 error) *DB_GetTalkgroupWithLearned_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *DB_GetTalkgroupWithLearned_Call) RunAndReturn(run func(context.Context, int32, int32) (database.GetTalkgroupWithLearnedRow, error)) *DB_GetTalkgroupWithLearned_Call { + _c.Call.Return(run) + return _c +} + +// GetTalkgroupsBySysTGID provides a mock function with given fields: ctx, ids +func (_m *DB) GetTalkgroupsBySysTGID(ctx context.Context, ids database.TGTuples) ([]database.GetTalkgroupsRow, error) { + ret := _m.Called(ctx, ids) + + if len(ret) == 0 { + panic("no return value specified for GetTalkgroupsBySysTGID") + } + + var r0 []database.GetTalkgroupsRow + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, database.TGTuples) ([]database.GetTalkgroupsRow, error)); ok { + return rf(ctx, ids) + } + if rf, ok := ret.Get(0).(func(context.Context, database.TGTuples) []database.GetTalkgroupsRow); ok { + r0 = rf(ctx, ids) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]database.GetTalkgroupsRow) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, database.TGTuples) error); ok { + r1 = rf(ctx, ids) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// DB_GetTalkgroupsBySysTGID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetTalkgroupsBySysTGID' +type DB_GetTalkgroupsBySysTGID_Call struct { + *mock.Call +} + +// GetTalkgroupsBySysTGID is a helper method to define mock.On call +// - ctx context.Context +// - ids database.TGTuples +func (_e *DB_Expecter) GetTalkgroupsBySysTGID(ctx interface{}, ids interface{}) *DB_GetTalkgroupsBySysTGID_Call { + return &DB_GetTalkgroupsBySysTGID_Call{Call: _e.mock.On("GetTalkgroupsBySysTGID", ctx, ids)} +} + +func (_c *DB_GetTalkgroupsBySysTGID_Call) Run(run func(ctx context.Context, ids database.TGTuples)) *DB_GetTalkgroupsBySysTGID_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(database.TGTuples)) + }) + return _c +} + +func (_c *DB_GetTalkgroupsBySysTGID_Call) Return(_a0 []database.GetTalkgroupsRow, _a1 error) *DB_GetTalkgroupsBySysTGID_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *DB_GetTalkgroupsBySysTGID_Call) RunAndReturn(run func(context.Context, database.TGTuples) ([]database.GetTalkgroupsRow, error)) *DB_GetTalkgroupsBySysTGID_Call { + _c.Call.Return(run) + return _c +} + +// GetTalkgroupsWithAllTags provides a mock function with given fields: ctx, tags +func (_m *DB) GetTalkgroupsWithAllTags(ctx context.Context, tags []string) ([]database.GetTalkgroupsWithAllTagsRow, error) { + ret := _m.Called(ctx, tags) + + if len(ret) == 0 { + panic("no return value specified for GetTalkgroupsWithAllTags") + } + + var r0 []database.GetTalkgroupsWithAllTagsRow + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, []string) ([]database.GetTalkgroupsWithAllTagsRow, error)); ok { + return rf(ctx, tags) + } + if rf, ok := ret.Get(0).(func(context.Context, []string) []database.GetTalkgroupsWithAllTagsRow); ok { + r0 = rf(ctx, tags) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]database.GetTalkgroupsWithAllTagsRow) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, []string) error); ok { + r1 = rf(ctx, tags) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// DB_GetTalkgroupsWithAllTags_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetTalkgroupsWithAllTags' +type DB_GetTalkgroupsWithAllTags_Call struct { + *mock.Call +} + +// GetTalkgroupsWithAllTags is a helper method to define mock.On call +// - ctx context.Context +// - tags []string +func (_e *DB_Expecter) GetTalkgroupsWithAllTags(ctx interface{}, tags interface{}) *DB_GetTalkgroupsWithAllTags_Call { + return &DB_GetTalkgroupsWithAllTags_Call{Call: _e.mock.On("GetTalkgroupsWithAllTags", ctx, tags)} +} + +func (_c *DB_GetTalkgroupsWithAllTags_Call) Run(run func(ctx context.Context, tags []string)) *DB_GetTalkgroupsWithAllTags_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].([]string)) + }) + return _c +} + +func (_c *DB_GetTalkgroupsWithAllTags_Call) Return(_a0 []database.GetTalkgroupsWithAllTagsRow, _a1 error) *DB_GetTalkgroupsWithAllTags_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *DB_GetTalkgroupsWithAllTags_Call) RunAndReturn(run func(context.Context, []string) ([]database.GetTalkgroupsWithAllTagsRow, error)) *DB_GetTalkgroupsWithAllTags_Call { + _c.Call.Return(run) + return _c +} + +// GetTalkgroupsWithAnyTags provides a mock function with given fields: ctx, tags +func (_m *DB) GetTalkgroupsWithAnyTags(ctx context.Context, tags []string) ([]database.GetTalkgroupsWithAnyTagsRow, error) { + ret := _m.Called(ctx, tags) + + if len(ret) == 0 { + panic("no return value specified for GetTalkgroupsWithAnyTags") + } + + var r0 []database.GetTalkgroupsWithAnyTagsRow + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, []string) ([]database.GetTalkgroupsWithAnyTagsRow, error)); ok { + return rf(ctx, tags) + } + if rf, ok := ret.Get(0).(func(context.Context, []string) []database.GetTalkgroupsWithAnyTagsRow); ok { + r0 = rf(ctx, tags) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]database.GetTalkgroupsWithAnyTagsRow) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, []string) error); ok { + r1 = rf(ctx, tags) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// DB_GetTalkgroupsWithAnyTags_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetTalkgroupsWithAnyTags' +type DB_GetTalkgroupsWithAnyTags_Call struct { + *mock.Call +} + +// GetTalkgroupsWithAnyTags is a helper method to define mock.On call +// - ctx context.Context +// - tags []string +func (_e *DB_Expecter) GetTalkgroupsWithAnyTags(ctx interface{}, tags interface{}) *DB_GetTalkgroupsWithAnyTags_Call { + return &DB_GetTalkgroupsWithAnyTags_Call{Call: _e.mock.On("GetTalkgroupsWithAnyTags", ctx, tags)} +} + +func (_c *DB_GetTalkgroupsWithAnyTags_Call) Run(run func(ctx context.Context, tags []string)) *DB_GetTalkgroupsWithAnyTags_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].([]string)) + }) + return _c +} + +func (_c *DB_GetTalkgroupsWithAnyTags_Call) Return(_a0 []database.GetTalkgroupsWithAnyTagsRow, _a1 error) *DB_GetTalkgroupsWithAnyTags_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *DB_GetTalkgroupsWithAnyTags_Call) RunAndReturn(run func(context.Context, []string) ([]database.GetTalkgroupsWithAnyTagsRow, error)) *DB_GetTalkgroupsWithAnyTags_Call { + _c.Call.Return(run) + return _c +} + +// GetTalkgroupsWithLearned provides a mock function with given fields: ctx +func (_m *DB) GetTalkgroupsWithLearned(ctx context.Context) ([]database.GetTalkgroupsWithLearnedRow, error) { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for GetTalkgroupsWithLearned") + } + + var r0 []database.GetTalkgroupsWithLearnedRow + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) ([]database.GetTalkgroupsWithLearnedRow, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) []database.GetTalkgroupsWithLearnedRow); ok { + r0 = rf(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]database.GetTalkgroupsWithLearnedRow) + } + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// DB_GetTalkgroupsWithLearned_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetTalkgroupsWithLearned' +type DB_GetTalkgroupsWithLearned_Call struct { + *mock.Call +} + +// GetTalkgroupsWithLearned is a helper method to define mock.On call +// - ctx context.Context +func (_e *DB_Expecter) GetTalkgroupsWithLearned(ctx interface{}) *DB_GetTalkgroupsWithLearned_Call { + return &DB_GetTalkgroupsWithLearned_Call{Call: _e.mock.On("GetTalkgroupsWithLearned", ctx)} +} + +func (_c *DB_GetTalkgroupsWithLearned_Call) Run(run func(ctx context.Context)) *DB_GetTalkgroupsWithLearned_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *DB_GetTalkgroupsWithLearned_Call) Return(_a0 []database.GetTalkgroupsWithLearnedRow, _a1 error) *DB_GetTalkgroupsWithLearned_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *DB_GetTalkgroupsWithLearned_Call) RunAndReturn(run func(context.Context) ([]database.GetTalkgroupsWithLearnedRow, error)) *DB_GetTalkgroupsWithLearned_Call { + _c.Call.Return(run) + return _c +} + +// GetTalkgroupsWithLearnedBySysTGID provides a mock function with given fields: ctx, ids +func (_m *DB) GetTalkgroupsWithLearnedBySysTGID(ctx context.Context, ids database.TGTuples) ([]database.GetTalkgroupsRow, error) { + ret := _m.Called(ctx, ids) + + if len(ret) == 0 { + panic("no return value specified for GetTalkgroupsWithLearnedBySysTGID") + } + + var r0 []database.GetTalkgroupsRow + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, database.TGTuples) ([]database.GetTalkgroupsRow, error)); ok { + return rf(ctx, ids) + } + if rf, ok := ret.Get(0).(func(context.Context, database.TGTuples) []database.GetTalkgroupsRow); ok { + r0 = rf(ctx, ids) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]database.GetTalkgroupsRow) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, database.TGTuples) error); ok { + r1 = rf(ctx, ids) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// DB_GetTalkgroupsWithLearnedBySysTGID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetTalkgroupsWithLearnedBySysTGID' +type DB_GetTalkgroupsWithLearnedBySysTGID_Call struct { + *mock.Call +} + +// GetTalkgroupsWithLearnedBySysTGID is a helper method to define mock.On call +// - ctx context.Context +// - ids database.TGTuples +func (_e *DB_Expecter) GetTalkgroupsWithLearnedBySysTGID(ctx interface{}, ids interface{}) *DB_GetTalkgroupsWithLearnedBySysTGID_Call { + return &DB_GetTalkgroupsWithLearnedBySysTGID_Call{Call: _e.mock.On("GetTalkgroupsWithLearnedBySysTGID", ctx, ids)} +} + +func (_c *DB_GetTalkgroupsWithLearnedBySysTGID_Call) Run(run func(ctx context.Context, ids database.TGTuples)) *DB_GetTalkgroupsWithLearnedBySysTGID_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(database.TGTuples)) + }) + return _c +} + +func (_c *DB_GetTalkgroupsWithLearnedBySysTGID_Call) Return(_a0 []database.GetTalkgroupsRow, _a1 error) *DB_GetTalkgroupsWithLearnedBySysTGID_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *DB_GetTalkgroupsWithLearnedBySysTGID_Call) RunAndReturn(run func(context.Context, database.TGTuples) ([]database.GetTalkgroupsRow, error)) *DB_GetTalkgroupsWithLearnedBySysTGID_Call { + _c.Call.Return(run) + return _c +} + +// GetTalkgroupsWithLearnedBySystem provides a mock function with given fields: ctx, system +func (_m *DB) GetTalkgroupsWithLearnedBySystem(ctx context.Context, system int32) ([]database.GetTalkgroupsWithLearnedBySystemRow, error) { + ret := _m.Called(ctx, system) + + if len(ret) == 0 { + panic("no return value specified for GetTalkgroupsWithLearnedBySystem") + } + + var r0 []database.GetTalkgroupsWithLearnedBySystemRow + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, int32) ([]database.GetTalkgroupsWithLearnedBySystemRow, error)); ok { + return rf(ctx, system) + } + if rf, ok := ret.Get(0).(func(context.Context, int32) []database.GetTalkgroupsWithLearnedBySystemRow); ok { + r0 = rf(ctx, system) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]database.GetTalkgroupsWithLearnedBySystemRow) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, int32) error); ok { + r1 = rf(ctx, system) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// DB_GetTalkgroupsWithLearnedBySystem_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetTalkgroupsWithLearnedBySystem' +type DB_GetTalkgroupsWithLearnedBySystem_Call struct { + *mock.Call +} + +// GetTalkgroupsWithLearnedBySystem is a helper method to define mock.On call +// - ctx context.Context +// - system int32 +func (_e *DB_Expecter) GetTalkgroupsWithLearnedBySystem(ctx interface{}, system interface{}) *DB_GetTalkgroupsWithLearnedBySystem_Call { + return &DB_GetTalkgroupsWithLearnedBySystem_Call{Call: _e.mock.On("GetTalkgroupsWithLearnedBySystem", ctx, system)} +} + +func (_c *DB_GetTalkgroupsWithLearnedBySystem_Call) Run(run func(ctx context.Context, system int32)) *DB_GetTalkgroupsWithLearnedBySystem_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(int32)) + }) + return _c +} + +func (_c *DB_GetTalkgroupsWithLearnedBySystem_Call) Return(_a0 []database.GetTalkgroupsWithLearnedBySystemRow, _a1 error) *DB_GetTalkgroupsWithLearnedBySystem_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *DB_GetTalkgroupsWithLearnedBySystem_Call) RunAndReturn(run func(context.Context, int32) ([]database.GetTalkgroupsWithLearnedBySystemRow, error)) *DB_GetTalkgroupsWithLearnedBySystem_Call { + _c.Call.Return(run) + return _c +} + +// GetUserByID provides a mock function with given fields: ctx, id +func (_m *DB) GetUserByID(ctx context.Context, id int32) (database.User, error) { + ret := _m.Called(ctx, id) + + if len(ret) == 0 { + panic("no return value specified for GetUserByID") + } + + var r0 database.User + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, int32) (database.User, error)); ok { + return rf(ctx, id) + } + if rf, ok := ret.Get(0).(func(context.Context, int32) database.User); ok { + r0 = rf(ctx, id) + } else { + r0 = ret.Get(0).(database.User) + } + + if rf, ok := ret.Get(1).(func(context.Context, int32) error); ok { + r1 = rf(ctx, id) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// DB_GetUserByID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetUserByID' +type DB_GetUserByID_Call struct { + *mock.Call +} + +// GetUserByID is a helper method to define mock.On call +// - ctx context.Context +// - id int32 +func (_e *DB_Expecter) GetUserByID(ctx interface{}, id interface{}) *DB_GetUserByID_Call { + return &DB_GetUserByID_Call{Call: _e.mock.On("GetUserByID", ctx, id)} +} + +func (_c *DB_GetUserByID_Call) Run(run func(ctx context.Context, id int32)) *DB_GetUserByID_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(int32)) + }) + return _c +} + +func (_c *DB_GetUserByID_Call) Return(_a0 database.User, _a1 error) *DB_GetUserByID_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *DB_GetUserByID_Call) RunAndReturn(run func(context.Context, int32) (database.User, error)) *DB_GetUserByID_Call { + _c.Call.Return(run) + return _c +} + +// GetUserByUID provides a mock function with given fields: ctx, id +func (_m *DB) GetUserByUID(ctx context.Context, id int32) (database.User, error) { + ret := _m.Called(ctx, id) + + if len(ret) == 0 { + panic("no return value specified for GetUserByUID") + } + + var r0 database.User + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, int32) (database.User, error)); ok { + return rf(ctx, id) + } + if rf, ok := ret.Get(0).(func(context.Context, int32) database.User); ok { + r0 = rf(ctx, id) + } else { + r0 = ret.Get(0).(database.User) + } + + if rf, ok := ret.Get(1).(func(context.Context, int32) error); ok { + r1 = rf(ctx, id) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// DB_GetUserByUID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetUserByUID' +type DB_GetUserByUID_Call struct { + *mock.Call +} + +// GetUserByUID is a helper method to define mock.On call +// - ctx context.Context +// - id int32 +func (_e *DB_Expecter) GetUserByUID(ctx interface{}, id interface{}) *DB_GetUserByUID_Call { + return &DB_GetUserByUID_Call{Call: _e.mock.On("GetUserByUID", ctx, id)} +} + +func (_c *DB_GetUserByUID_Call) Run(run func(ctx context.Context, id int32)) *DB_GetUserByUID_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(int32)) + }) + return _c +} + +func (_c *DB_GetUserByUID_Call) Return(_a0 database.User, _a1 error) *DB_GetUserByUID_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *DB_GetUserByUID_Call) RunAndReturn(run func(context.Context, int32) (database.User, error)) *DB_GetUserByUID_Call { + _c.Call.Return(run) + return _c +} + +// GetUserByUsername provides a mock function with given fields: ctx, username +func (_m *DB) GetUserByUsername(ctx context.Context, username string) (database.User, error) { + ret := _m.Called(ctx, username) + + if len(ret) == 0 { + panic("no return value specified for GetUserByUsername") + } + + var r0 database.User + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (database.User, error)); ok { + return rf(ctx, username) + } + if rf, ok := ret.Get(0).(func(context.Context, string) database.User); ok { + r0 = rf(ctx, username) + } else { + r0 = ret.Get(0).(database.User) + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, username) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// DB_GetUserByUsername_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetUserByUsername' +type DB_GetUserByUsername_Call struct { + *mock.Call +} + +// GetUserByUsername is a helper method to define mock.On call +// - ctx context.Context +// - username string +func (_e *DB_Expecter) GetUserByUsername(ctx interface{}, username interface{}) *DB_GetUserByUsername_Call { + return &DB_GetUserByUsername_Call{Call: _e.mock.On("GetUserByUsername", ctx, username)} +} + +func (_c *DB_GetUserByUsername_Call) Run(run func(ctx context.Context, username string)) *DB_GetUserByUsername_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string)) + }) + return _c +} + +func (_c *DB_GetUserByUsername_Call) Return(_a0 database.User, _a1 error) *DB_GetUserByUsername_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *DB_GetUserByUsername_Call) RunAndReturn(run func(context.Context, string) (database.User, error)) *DB_GetUserByUsername_Call { + _c.Call.Return(run) + return _c +} + +// GetUsers provides a mock function with given fields: ctx +func (_m *DB) GetUsers(ctx context.Context) ([]database.User, error) { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for GetUsers") + } + + var r0 []database.User + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) ([]database.User, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) []database.User); ok { + r0 = rf(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]database.User) + } + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// DB_GetUsers_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetUsers' +type DB_GetUsers_Call struct { + *mock.Call +} + +// GetUsers is a helper method to define mock.On call +// - ctx context.Context +func (_e *DB_Expecter) GetUsers(ctx interface{}) *DB_GetUsers_Call { + return &DB_GetUsers_Call{Call: _e.mock.On("GetUsers", ctx)} +} + +func (_c *DB_GetUsers_Call) Run(run func(ctx context.Context)) *DB_GetUsers_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *DB_GetUsers_Call) Return(_a0 []database.User, _a1 error) *DB_GetUsers_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *DB_GetUsers_Call) RunAndReturn(run func(context.Context) ([]database.User, error)) *DB_GetUsers_Call { + _c.Call.Return(run) + return _c +} + +// SetCallTranscript provides a mock function with given fields: ctx, iD, transcript +func (_m *DB) SetCallTranscript(ctx context.Context, iD uuid.UUID, transcript *string) error { + ret := _m.Called(ctx, iD, transcript) + + if len(ret) == 0 { + panic("no return value specified for SetCallTranscript") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, uuid.UUID, *string) error); ok { + r0 = rf(ctx, iD, transcript) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// DB_SetCallTranscript_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetCallTranscript' +type DB_SetCallTranscript_Call struct { + *mock.Call +} + +// SetCallTranscript is a helper method to define mock.On call +// - ctx context.Context +// - iD uuid.UUID +// - transcript *string +func (_e *DB_Expecter) SetCallTranscript(ctx interface{}, iD interface{}, transcript interface{}) *DB_SetCallTranscript_Call { + return &DB_SetCallTranscript_Call{Call: _e.mock.On("SetCallTranscript", ctx, iD, transcript)} +} + +func (_c *DB_SetCallTranscript_Call) Run(run func(ctx context.Context, iD uuid.UUID, transcript *string)) *DB_SetCallTranscript_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(uuid.UUID), args[2].(*string)) + }) + return _c +} + +func (_c *DB_SetCallTranscript_Call) Return(_a0 error) *DB_SetCallTranscript_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *DB_SetCallTranscript_Call) RunAndReturn(run func(context.Context, uuid.UUID, *string) error) *DB_SetCallTranscript_Call { + _c.Call.Return(run) + return _c +} + +// SetTalkgroupTags provides a mock function with given fields: ctx, tags, systemID, tgID +func (_m *DB) SetTalkgroupTags(ctx context.Context, tags []string, systemID int32, tgID int32) error { + ret := _m.Called(ctx, tags, systemID, tgID) + + if len(ret) == 0 { + panic("no return value specified for SetTalkgroupTags") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, []string, int32, int32) error); ok { + r0 = rf(ctx, tags, systemID, tgID) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// DB_SetTalkgroupTags_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetTalkgroupTags' +type DB_SetTalkgroupTags_Call struct { + *mock.Call +} + +// SetTalkgroupTags is a helper method to define mock.On call +// - ctx context.Context +// - tags []string +// - systemID int32 +// - tgID int32 +func (_e *DB_Expecter) SetTalkgroupTags(ctx interface{}, tags interface{}, systemID interface{}, tgID interface{}) *DB_SetTalkgroupTags_Call { + return &DB_SetTalkgroupTags_Call{Call: _e.mock.On("SetTalkgroupTags", ctx, tags, systemID, tgID)} +} + +func (_c *DB_SetTalkgroupTags_Call) Run(run func(ctx context.Context, tags []string, systemID int32, tgID int32)) *DB_SetTalkgroupTags_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].([]string), args[2].(int32), args[3].(int32)) + }) + return _c +} + +func (_c *DB_SetTalkgroupTags_Call) Return(_a0 error) *DB_SetTalkgroupTags_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *DB_SetTalkgroupTags_Call) RunAndReturn(run func(context.Context, []string, int32, int32) error) *DB_SetTalkgroupTags_Call { + _c.Call.Return(run) + return _c +} + +// UpdatePassword provides a mock function with given fields: ctx, username, password +func (_m *DB) UpdatePassword(ctx context.Context, username string, password string) error { + ret := _m.Called(ctx, username, password) + + if len(ret) == 0 { + panic("no return value specified for UpdatePassword") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) error); ok { + r0 = rf(ctx, username, password) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// DB_UpdatePassword_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdatePassword' +type DB_UpdatePassword_Call struct { + *mock.Call +} + +// UpdatePassword is a helper method to define mock.On call +// - ctx context.Context +// - username string +// - password string +func (_e *DB_Expecter) UpdatePassword(ctx interface{}, username interface{}, password interface{}) *DB_UpdatePassword_Call { + return &DB_UpdatePassword_Call{Call: _e.mock.On("UpdatePassword", ctx, username, password)} +} + +func (_c *DB_UpdatePassword_Call) Run(run func(ctx context.Context, username string, password string)) *DB_UpdatePassword_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string), args[2].(string)) + }) + return _c +} + +func (_c *DB_UpdatePassword_Call) Return(_a0 error) *DB_UpdatePassword_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *DB_UpdatePassword_Call) RunAndReturn(run func(context.Context, string, string) error) *DB_UpdatePassword_Call { + _c.Call.Return(run) + return _c +} + +// UpdateTalkgroup provides a mock function with given fields: ctx, arg +func (_m *DB) UpdateTalkgroup(ctx context.Context, arg database.UpdateTalkgroupParams) (database.Talkgroup, error) { + ret := _m.Called(ctx, arg) + + if len(ret) == 0 { + panic("no return value specified for UpdateTalkgroup") + } + + var r0 database.Talkgroup + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, database.UpdateTalkgroupParams) (database.Talkgroup, error)); ok { + return rf(ctx, arg) + } + if rf, ok := ret.Get(0).(func(context.Context, database.UpdateTalkgroupParams) database.Talkgroup); ok { + r0 = rf(ctx, arg) + } else { + r0 = ret.Get(0).(database.Talkgroup) + } + + if rf, ok := ret.Get(1).(func(context.Context, database.UpdateTalkgroupParams) error); ok { + r1 = rf(ctx, arg) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// DB_UpdateTalkgroup_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateTalkgroup' +type DB_UpdateTalkgroup_Call struct { + *mock.Call +} + +// UpdateTalkgroup is a helper method to define mock.On call +// - ctx context.Context +// - arg database.UpdateTalkgroupParams +func (_e *DB_Expecter) UpdateTalkgroup(ctx interface{}, arg interface{}) *DB_UpdateTalkgroup_Call { + return &DB_UpdateTalkgroup_Call{Call: _e.mock.On("UpdateTalkgroup", ctx, arg)} +} + +func (_c *DB_UpdateTalkgroup_Call) Run(run func(ctx context.Context, arg database.UpdateTalkgroupParams)) *DB_UpdateTalkgroup_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(database.UpdateTalkgroupParams)) + }) + return _c +} + +func (_c *DB_UpdateTalkgroup_Call) Return(_a0 database.Talkgroup, _a1 error) *DB_UpdateTalkgroup_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *DB_UpdateTalkgroup_Call) RunAndReturn(run func(context.Context, database.UpdateTalkgroupParams) (database.Talkgroup, error)) *DB_UpdateTalkgroup_Call { + _c.Call.Return(run) + return _c +} + +// NewDB creates a new instance of DB. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewDB(t interface { + mock.TestingT + Cleanup(func()) +}) *DB { + mock := &DB{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/talkgroups/importer/import.go b/pkg/talkgroups/importer/import.go index 5d0493a..82730fe 100644 --- a/pkg/talkgroups/importer/import.go +++ b/pkg/talkgroups/importer/import.go @@ -9,7 +9,6 @@ import ( "regexp" "strconv" "strings" - "time" "github.com/google/uuid" @@ -74,11 +73,6 @@ func (rr *radioReferenceImporter) importTalkgroups(ctx context.Context, sys int, return nil, talkgroups.ErrNoSuchSystem } - importedFrom := jsontypes.Metadata{ - "from": "RadioReference", - "time": time.Now(), - } - var groupName string state := rrsInitial for sc.Scan() { @@ -107,12 +101,11 @@ func (rr *radioReferenceImporter) importTalkgroups(ctx context.Context, sys int, if err != nil { continue } - metadata := jsontypes.Metadata{ - "imported": importedFrom, - } + var metadata jsontypes.Metadata tgt := talkgroups.TG(sys, tgid) mode := fields[2] if strings.Contains(mode, "E") { + metadata = make(jsontypes.Metadata) metadata["encrypted"] = true } tags := []string{fields[5]} diff --git a/pkg/talkgroups/importer/import_test.go b/pkg/talkgroups/importer/import_test.go index 7ca74c2..9957f47 100644 --- a/pkg/talkgroups/importer/import_test.go +++ b/pkg/talkgroups/importer/import_test.go @@ -3,15 +3,19 @@ package importer_test import ( "context" "encoding/json" + "math/rand" "os" "testing" + "github.com/google/uuid" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" - "dynatron.me/x/stillbox/pkg/talkgroups/importer" "dynatron.me/x/stillbox/pkg/database" "dynatron.me/x/stillbox/pkg/database/mocks" + "dynatron.me/x/stillbox/pkg/talkgroups" + "dynatron.me/x/stillbox/pkg/talkgroups/importer" ) func getFixture(fixture string) []byte { @@ -20,34 +24,49 @@ func getFixture(fixture string) []byte { panic(err) } - return fixt } -func TestRadioReferenceImport(t *testing.T) { - tests := []struct{ - name string - input []byte - sysID int - jsExpect []byte +func TestImport(t *testing.T) { + // this is for deterministic UUIDs + uuid.SetRand(rand.New(rand.NewSource(1))) + + tests := []struct { + name string + input []byte + impType string + sysID int + sysName string + jsExpect []byte expectErr error }{ { - name: "base", - input: getFixture("riscon.txt"), + name: "radioreference", + impType: "radioreference", + input: getFixture("riscon.txt"), jsExpect: getFixture("riscon.json"), - sysID: 197, + sysID: 197, + sysName: "RISCON", + }, + { + name: "unknown importer", + impType: "nonexistent", + expectErr: importer.ErrBadImportType, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - ctx := context.Background() - ctx = database.CtxWithDB(ctx, mocks.NewDB()) + dbMock := mocks.NewDB(t) + if tc.expectErr == nil { + dbMock.EXPECT().GetSystemName(mock.AnythingOfType("*context.valueCtx"), tc.sysID).Return(tc.sysName, nil) + } + ctx := database.CtxWithDB(context.Background(), dbMock) + ctx = talkgroups.CtxWithStore(ctx, talkgroups.NewCache()) ij := &importer.ImportJob{ - Type: "radioreference", + Type: importer.ImportSource(tc.impType), SystemID: tc.sysID, - Body: string(tc.input), + Body: string(tc.input), } tgs, err := ij.Import(ctx) @@ -58,10 +77,12 @@ func TestRadioReferenceImport(t *testing.T) { } else { require.NoError(t, err) - jse, jerr := json.Marshal(tgs) - require.NoError(t, jerr) + var fixt []talkgroups.Talkgroup + err = json.Unmarshal(tc.jsExpect, &fixt) + // jse, _ := json.Marshal(tgs); os.WriteFile("testdata/riscon.json", jse, 0600) + require.NoError(t, err) - assert.Equal(t, tc.jsExpect, jse) + assert.Equal(t, fixt, tgs) } }) diff --git a/pkg/talkgroups/importer/testdata/riscon.json b/pkg/talkgroups/importer/testdata/riscon.json index e69de29..5a0c63e 100644 --- a/pkg/talkgroups/importer/testdata/riscon.json +++ b/pkg/talkgroups/importer/testdata/riscon.json @@ -0,0 +1 @@ +[{"id":"52fdfc07-2182-454f-963f-5f0f9a621d72","system_id":197,"tgid":2,"name":"Intercity Fire","alpha_tag":"Intercity FD","tg_group":"Statewide Mutual Aid/Intersystem","frequency":null,"metadata":null,"tags":["Interop"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"9566c74d-1003-4c4d-bbbb-0407d1e2c649","system_id":197,"tgid":3,"name":"Intercity Police","alpha_tag":"Intercity PD","tg_group":"Statewide Mutual Aid/Intersystem","frequency":null,"metadata":null,"tags":["Interop"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"81855ad8-681d-4d86-91e9-1e00167939cb","system_id":197,"tgid":21,"name":"North Dispatch ","alpha_tag":"RISP N Disp","tg_group":"State Police - District A (North)","frequency":null,"metadata":null,"tags":["Law Dispatch"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"6694d2c4-22ac-4208-a007-2939487f6999","system_id":197,"tgid":22,"name":"North Car-to-Car/Information","alpha_tag":"RISP N Car","tg_group":"State Police - District A (North)","frequency":null,"metadata":{"encrypted":true},"tags":["Law Talk"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"eb9d18a4-4784-445d-87f3-c67cf22746e9","system_id":197,"tgid":24,"name":"North Tactical Ops 1","alpha_tag":"RISP N Tac 1","tg_group":"State Police - District A (North)","frequency":null,"metadata":{"encrypted":true},"tags":["Law Tac"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"95af5a25-3679-41ba-a2ff-6cd471c483f1","system_id":197,"tgid":23,"name":"North Tactical Ops 2","alpha_tag":"RISP N Tac 2","tg_group":"State Police - District A (North)","frequency":null,"metadata":{"encrypted":true},"tags":["Law Tac"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"5fb90bad-b37c-4821-b6d9-5526a41a9504","system_id":197,"tgid":25,"name":"South Dispatch ","alpha_tag":"RISP S Disp","tg_group":"State Police - District B (South)","frequency":null,"metadata":null,"tags":["Law Dispatch"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"680b4e7c-8b76-4a1b-9d49-d4955c848621","system_id":197,"tgid":27,"name":"South Car-to-Car/Information","alpha_tag":"RISP S Car","tg_group":"State Police - District B (South)","frequency":null,"metadata":{"encrypted":true},"tags":["Law Talk"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"6325253f-ec73-4dd7-a9e2-8bf921119c16","system_id":197,"tgid":16,"name":"State Fire Marshall","alpha_tag":"State FMO","tg_group":"Statewide Fire","frequency":null,"metadata":null,"tags":["Fire-Talk"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"0f070244-8615-4bda-8831-3f6a8eb668d2","system_id":197,"tgid":1038,"name":"Northern Rhode Island Fire Chiefs","alpha_tag":"NRI Fire Chi","tg_group":"Statewide Fire","frequency":null,"metadata":null,"tags":["Fire-Talk"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"0bf50598-7592-4e66-8a5b-df2c7fc48445","system_id":197,"tgid":1041,"name":"Southern Rhode Island Fire Chiefs","alpha_tag":"SRI Fire Chi","tg_group":"Statewide Fire","frequency":null,"metadata":null,"tags":["Fire-Talk"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"92d2572b-cd06-48d2-96c5-2f5054e2d083","system_id":197,"tgid":1314,"name":"Tanker Taskforce 1","alpha_tag":"Tanker TF 1","tg_group":"Statewide Fire","frequency":null,"metadata":null,"tags":["Fire-Talk"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"6bf84c71-74cb-4476-b64c-c3dbd968b0f7","system_id":197,"tgid":194,"name":"Lifepact Ambulance (Statewide)","alpha_tag":"Lifepact Amb","tg_group":"Statewide EMS and Hospitals","frequency":null,"metadata":null,"tags":["EMS Dispatch"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"172ed857-94bb-458b-8c3b-525da1786f9f","system_id":197,"tgid":212,"name":"Fatima St Josephs","alpha_tag":"Fatima-St Joes","tg_group":"Statewide EMS and Hospitals","frequency":null,"metadata":null,"tags":["Business"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"ff094279-db19-44eb-97a1-9d0f7bbacbe0","system_id":197,"tgid":220,"name":"Health 1","alpha_tag":"Health 1","tg_group":"Statewide EMS and Hospitals","frequency":null,"metadata":{"encrypted":true},"tags":["EMS-Talk"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"255aa5b7-d44b-4c40-b84c-892b9bffd436","system_id":197,"tgid":221,"name":"Health 2","alpha_tag":"Health 2","tg_group":"Statewide EMS and Hospitals","frequency":null,"metadata":{"encrypted":true},"tags":["EMS-Talk"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"29b0223b-eea5-44f7-8391-f445d15afd42","system_id":197,"tgid":222,"name":"Department of Health - Statewide","alpha_tag":"Dept of HealthSW","tg_group":"Statewide EMS and Hospitals","frequency":null,"metadata":null,"tags":["EMS-Talk"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"94040374-f692-4b98-8bf8-713f8d962d7c","system_id":197,"tgid":228,"name":"DMAT South","alpha_tag":"DMAT South","tg_group":"Statewide EMS and Hospitals","frequency":null,"metadata":{"encrypted":true},"tags":["Emergency Ops"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"8d019192-c242-44e2-8afc-cae3a61fb586","system_id":197,"tgid":232,"name":"Life Span Net 1","alpha_tag":"Life Span 1","tg_group":"Statewide EMS and Hospitals","frequency":null,"metadata":null,"tags":["EMS-Tac"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"b14323a6-bc8f-4e7d-b1d9-29333ff99393","system_id":197,"tgid":234,"name":"RI Hospital Operations","alpha_tag":"RI Hosp Ops","tg_group":"Statewide EMS and Hospitals","frequency":null,"metadata":null,"tags":["Business"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"3bea6f5b-3af6-4e03-b436-6c4719e43a1b","system_id":197,"tgid":120,"name":"Law Enforcement Operations","alpha_tag":"DEM PD Ops","tg_group":"Department of Environmental Management","frequency":null,"metadata":null,"tags":["Law Dispatch"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"067d89bc-7f01-41f5-b398-1659a44ff17a","system_id":197,"tgid":122,"name":"Law Enforcement Police","alpha_tag":"DEM Police","tg_group":"Department of Environmental Management","frequency":null,"metadata":null,"tags":["Law Talk"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"4c7215a3-b539-4b1e-9849-c6077dbb5722","system_id":197,"tgid":10,"name":"Emergency Management Agency 1","alpha_tag":"EMA-1","tg_group":"Emergency Management Agency","frequency":null,"metadata":null,"tags":["Emergency Ops"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"f5717a28-9a26-4f97-a479-81998ebea89c","system_id":197,"tgid":20,"name":"Emergency Management Agency","alpha_tag":"EMA","tg_group":"Emergency Management Agency","frequency":null,"metadata":null,"tags":["Emergency Ops"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"0b4b3739-7011-4e82-ad6f-4125c8fa7311","system_id":197,"tgid":4,"name":"Wide Area 3","alpha_tag":"Wide Area 3","tg_group":"Statewide Area/Events","frequency":null,"metadata":null,"tags":["Interop"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"e4d7defa-922d-4ae7-b866-67f7e936cd4f","system_id":197,"tgid":5,"name":"Wide Area 4","alpha_tag":"Wide Area 4","tg_group":"Statewide Area/Events","frequency":null,"metadata":null,"tags":["Interop"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"24abf7df-866b-4a56-8383-67ad6145de1e","system_id":197,"tgid":6,"name":"Wide Area 5","alpha_tag":"Wide Area 5","tg_group":"Statewide Area/Events","frequency":null,"metadata":null,"tags":["Interop"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"e8f4a8b0-993e-4df8-883a-0ad8be9c3978","system_id":197,"tgid":7,"name":"Wide Area 6","alpha_tag":"Wide Area 6","tg_group":"Statewide Area/Events","frequency":null,"metadata":null,"tags":["Interop"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"b04883e5-6a15-4a8d-a563-afa467d49dec","system_id":197,"tgid":1018,"name":"Southwide CH-1","alpha_tag":"SOUTHWIDE 1","tg_group":"Statewide Area/Events","frequency":null,"metadata":null,"tags":["Interop"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"6a40e9a1-d007-4033-8282-3061bdd0eaa5","system_id":197,"tgid":1019,"name":"Southwide CH-2","alpha_tag":"SOUTHWIDE 2","tg_group":"Statewide Area/Events","frequency":null,"metadata":null,"tags":["Interop"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"9f8e4da6-4301-4522-8d0b-29688b734b8e","system_id":197,"tgid":1022,"name":"Wide Area 7","alpha_tag":"WIDE AREA 7","tg_group":"Statewide Area/Events","frequency":null,"metadata":null,"tags":["Interop"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"a0f3ca99-36e8-461f-90d7-7c96ea80a7a6","system_id":197,"tgid":1023,"name":"Wide Area 8","alpha_tag":"WIDE AREA 8","tg_group":"Statewide Area/Events","frequency":null,"metadata":{"encrypted":true},"tags":["Interop"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"65f606f6-a63b-4f3d-bd25-67c18979e4d6","system_id":197,"tgid":1025,"name":"Inland Marine Interop","alpha_tag":"Inland Marine IO","tg_group":"Statewide Area/Events","frequency":null,"metadata":null,"tags":["Interop"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"0f26686d-9bf2-4b26-8901-ff354cde1607","system_id":197,"tgid":1037,"name":"Southside CH 5","alpha_tag":"SOUTHSIDE 5","tg_group":"Statewide Area/Events","frequency":null,"metadata":{"encrypted":true},"tags":["Interop"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"ee294b39-f32b-4c78-a2ba-64f84ab43ca0","system_id":197,"tgid":1173,"name":"North Wide 1","alpha_tag":"NORTHWIDE1","tg_group":"Statewide Area/Events","frequency":null,"metadata":null,"tags":["Interop"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"c6e6b91c-1fd3-4e89-9043-4179d3af4491","system_id":197,"tgid":1174,"name":"North Wide 2","alpha_tag":"NORTHWIDE2","tg_group":"Statewide Area/Events","frequency":null,"metadata":null,"tags":["Interop"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"a369012d-b92d-484f-839d-1734ff571642","system_id":197,"tgid":1177,"name":"North Wide 5","alpha_tag":"NORTHWIDE5","tg_group":"Statewide Area/Events","frequency":null,"metadata":{"encrypted":true},"tags":["Interop"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"8953bb68-65fc-492b-8c3a-17c9028be991","system_id":197,"tgid":1185,"name":"Metro Wide 1","alpha_tag":"METROWIDE1","tg_group":"Statewide Area/Events","frequency":null,"metadata":null,"tags":["Interop"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"4eb7649c-6c93-4780-8979-d1830356f2a5","system_id":197,"tgid":1186,"name":"Metro Wide 2","alpha_tag":"METROWIDE2","tg_group":"Statewide Area/Events","frequency":null,"metadata":null,"tags":["Interop"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"4c3deab2-a4b4-475d-a3af-be8fb56987c7","system_id":197,"tgid":1187,"name":"Metro Wide 3","alpha_tag":"METROWIDE3","tg_group":"Statewide Area/Events","frequency":null,"metadata":{"encrypted":true},"tags":["Interop"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"7f581852-6f18-44be-8233-50eab13935f3","system_id":197,"tgid":1335,"name":"East Wide 1","alpha_tag":"EASTWIDE 1","tg_group":"Statewide Area/Events","frequency":null,"metadata":null,"tags":["Interop"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"1d844845-17e9-44ae-b78a-e151c0075592","system_id":197,"tgid":1336,"name":"East Wide 2","alpha_tag":"EASTWIDE 2","tg_group":"Statewide Area/Events","frequency":null,"metadata":null,"tags":["Interop"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"5836b707-5885-450c-b0ec-29a3703934bf","system_id":197,"tgid":1337,"name":"East Wide 3","alpha_tag":"EASTWIDE 3","tg_group":"Statewide Area/Events","frequency":null,"metadata":{"encrypted":true},"tags":["Interop"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"50a28da1-0297-4ded-a77e-758579ea3dfe","system_id":197,"tgid":11186,"name":"Metro Wide 2","alpha_tag":"METROWIDE2","tg_group":"Statewide Area/Events","frequency":null,"metadata":null,"tags":["Interop"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"4136abf7-52b3-4827-9d03-e944b3c9db36","system_id":197,"tgid":1033,"name":"Tanker Taskforce ","alpha_tag":"TANK TF","tg_group":"Statewide Emergency Response","frequency":null,"metadata":null,"tags":["Fire-Tac"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"6b75045f-8efd-49d2-aae5-411947cb553d","system_id":197,"tgid":1034,"name":"Hazmat 1","alpha_tag":"HZT DC1","tg_group":"Statewide Emergency Response","frequency":null,"metadata":null,"tags":["Fire-Tac"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"7694267a-ef4e-4cea-806b-32d6108bd685","system_id":197,"tgid":1035,"name":"Hazmat 2","alpha_tag":"HZT DC2","tg_group":"Statewide Emergency Response","frequency":null,"metadata":null,"tags":["Fire-Tac"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"84f57e37-caac-4e33-beaa-3263a3994370","system_id":197,"tgid":176,"name":"Department of Transportation - Primary","alpha_tag":"RIDOT Primary","tg_group":"Department of Transportation","frequency":null,"metadata":null,"tags":["Public Works"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"24ba9c9b-1467-4a27-8f01-a910ae295f6e","system_id":197,"tgid":4421,"name":"Newport Pell Bridge Operations","alpha_tag":"RITBA - Pell Bdg","tg_group":"Tunnel and Bridge Authority","frequency":null,"metadata":null,"tags":["Public Works"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"fbfe5f5a-bf44-4cde-a63b-5606633e2bf0","system_id":197,"tgid":274,"name":"Providence VA Police","alpha_tag":"VA Police","tg_group":"Federal","frequency":null,"metadata":null,"tags":["Law Dispatch"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"006f2829-5d7d-4906-9f01-a239c4365854","system_id":197,"tgid":186,"name":"Rhode Island Public Transit Auth.","alpha_tag":"RIPTA","tg_group":"RIPTA","frequency":null,"metadata":{"encrypted":true},"tags":["Transportation"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"c3af7f6b-41d6-41f9-ab9a-8d12f4125732","system_id":197,"tgid":187,"name":"Rhode Island Public Transit Auth.","alpha_tag":"RIPTA","tg_group":"RIPTA","frequency":null,"metadata":null,"tags":["Transportation"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"5fff332f-7576-4062-8556-304a3e3eae14","system_id":197,"tgid":188,"name":"Rhode Island Public Transit Auth.","alpha_tag":"RIPTA","tg_group":"RIPTA","frequency":null,"metadata":null,"tags":["Transportation"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"c28d0cea-39d2-401a-9272-0da85ca1e4b3","system_id":197,"tgid":189,"name":"Rhode Island Public Transit Auth.","alpha_tag":"RIPTA","tg_group":"RIPTA","frequency":null,"metadata":null,"tags":["Transportation"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"8eaf3f44-c6c6-4f83-a2f2-f54fc00e09d6","system_id":197,"tgid":190,"name":"Rhode Island Public Transit. Auth.","alpha_tag":"RIPTA","tg_group":"RIPTA","frequency":null,"metadata":null,"tags":["Transportation"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"fc256408-54c1-4dfc-acaa-8a2cecce5a3a","system_id":197,"tgid":304,"name":"Fire Operations","alpha_tag":"Quonset ANGB FD","tg_group":"Quonset ANGB","frequency":null,"metadata":null,"tags":["Fire Dispatch"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"ba53ab70-5b18-4b94-b4d3-38a5143e6340","system_id":197,"tgid":17,"name":"Airport Police Operations","alpha_tag":"TF Green PD","tg_group":"Rhode Island Airport Commission","frequency":null,"metadata":{"encrypted":true},"tags":["Law Dispatch"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"8d8724b0-cf3f-4e17-a3f7-9be1072fb63c","system_id":197,"tgid":19,"name":"Airport Fire Operations","alpha_tag":"TF Green FD","tg_group":"Rhode Island Airport Commission","frequency":null,"metadata":null,"tags":["Fire Dispatch"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"35d6042c-4160-438e-a9e2-a9f3fb4ffb00","system_id":197,"tgid":1126,"name":"University of Rhode Island Police - Dispatch","alpha_tag":"URI PD","tg_group":"College/Education Security","frequency":null,"metadata":{"encrypted":true},"tags":["Law Dispatch"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"19b454d5-22b5-4fa1-b604-193fb8966710","system_id":197,"tgid":1131,"name":"University of Rhode Island - EMS","alpha_tag":"URI EMS","tg_group":"College/Education Security","frequency":null,"metadata":{"encrypted":true},"tags":["EMS Dispatch"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"a7960732-ca52-4f53-83f5-20c889b79bf5","system_id":197,"tgid":1348,"name":"St. George's School (Middletown) - Security","alpha_tag":"St George Sec","tg_group":"College/Education Security","frequency":null,"metadata":null,"tags":["Security"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"04cfb57c-7601-432d-989b-accea9d6e263","system_id":197,"tgid":10228,"name":"Rhode Island School of Design - Security","alpha_tag":"RISD Secuty","tg_group":"College/Education Security","frequency":null,"metadata":{"encrypted":true},"tags":["Security"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"e25c2774-1d3f-4c62-8bbb-15d9afbcbf7f","system_id":197,"tgid":10229,"name":"Providence College Security - Dispatch","alpha_tag":"PROV COLL","tg_group":"College/Education Security","frequency":null,"metadata":{"encrypted":true},"tags":["Security"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"7da41ab0-408e-4969-82e2-cdcf233438bf","system_id":197,"tgid":10230,"name":"Rhode Island College Security","alpha_tag":"RI COL SEC","tg_group":"College/Education Security","frequency":null,"metadata":null,"tags":["Security"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"1774ace7-709a-4f09-9e9a-83fdeae0ec55","system_id":197,"tgid":11001,"name":"Brown University Police - Dispatch","alpha_tag":"BROWN UNIV","tg_group":"College/Education Security","frequency":null,"metadata":{"encrypted":true},"tags":["Law Dispatch"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"eb233a9b-5394-4b3c-b856-b546d313c8a3","system_id":197,"tgid":11002,"name":"Brown University Police - Car-to-Car","alpha_tag":"BROWN CAR","tg_group":"College/Education Security","frequency":null,"metadata":{"encrypted":true},"tags":["Law Talk"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"b4c1c0e0-5447-44ba-b70e-b36dbcfdec90","system_id":197,"tgid":11003,"name":"Brown University Police - Tactical","alpha_tag":"BROWN TAC","tg_group":"College/Education Security","frequency":null,"metadata":{"encrypted":true},"tags":["Law Tac"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"b302dcdc-3b9e-4522-a2a6-f1ed0afec1f8","system_id":197,"tgid":12,"name":"Metro Wide 2","alpha_tag":"METROWIDE2","tg_group":"Statewide Misc.","frequency":null,"metadata":{"encrypted":true},"tags":["Interop"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"e20faabe-df6b-462e-b17d-3a748a58677a","system_id":197,"tgid":14,"name":"Metro Wide 4","alpha_tag":"METROWIDE4","tg_group":"Statewide Misc.","frequency":null,"metadata":{"encrypted":true},"tags":["Interop"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"0c56348f-8921-4266-b11d-0f334c62fe52","system_id":197,"tgid":70,"name":"RI Traffic Tribunal Security","alpha_tag":"TFC TRIBUNAL","tg_group":"Statewide Misc.","frequency":null,"metadata":{"encrypted":true},"tags":["Security"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"ba53af19-779c-4294-8b65-70ffa0b77396","system_id":197,"tgid":168,"name":"Rhode Island Red Cross - Primary","alpha_tag":"Red Cross 1","tg_group":"Statewide Misc.","frequency":null,"metadata":null,"tags":["Other"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"3c130ad7-97dd-4afe-8e3a-d29b5125210f","system_id":197,"tgid":169,"name":"Rhode Island Red Cross - Secondary","alpha_tag":"Red Cross 2","tg_group":"Statewide Misc.","frequency":null,"metadata":null,"tags":["Other"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"0ef1c314-090f-47c7-9a6f-571c246f3e9a","system_id":197,"tgid":223,"name":"Statewide Nursing Homes Net","alpha_tag":"NURSING HM","tg_group":"Statewide Misc.","frequency":null,"metadata":null,"tags":["Other"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"c0b7413e-f110-4d58-b00c-e73bff706f7f","system_id":197,"tgid":243,"name":"Hospital Operations","alpha_tag":"Slater Hosp Ops","tg_group":"Statewide Misc.","frequency":null,"metadata":null,"tags":["Business"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"f4b6f440-90a3-4711-b320-8e4e4b89cb51","system_id":197,"tgid":244,"name":"Slater Hospital Security","alpha_tag":"Slater Hosp Sec","tg_group":"Statewide Misc.","frequency":null,"metadata":null,"tags":["Security"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"65ce6400-2cbd-4c28-87aa-113df2468928","system_id":197,"tgid":1042,"name":"County Fireground","alpha_tag":"WashCo FireG","tg_group":"Washington County","frequency":null,"metadata":null,"tags":["Fire-Tac"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"d5a23b9c-a740-480c-9382-d9c6034ad296","system_id":197,"tgid":1479,"name":"County Fire Station/Station","alpha_tag":"WashCo FireS","tg_group":"Washington County","frequency":null,"metadata":null,"tags":["Fire-Talk"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"0c796503-e1ce-4217-a5f5-0caf1fbfe831","system_id":197,"tgid":1712,"name":"Fire 1 Dispatch","alpha_tag":"BarringtnFD1","tg_group":"Barrington","frequency":null,"metadata":null,"tags":["Fire Dispatch"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"b10b7bf5-b15c-47a5-bdbf-8e7dcafc9e13","system_id":197,"tgid":1713,"name":"Fire 2","alpha_tag":"BarringtnFD2","tg_group":"Barrington","frequency":null,"metadata":null,"tags":["Fire-Tac"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"8647a4b4-4ed4-4ce9-a4ed-47f74aa59446","system_id":197,"tgid":1715,"name":"Police Operations","alpha_tag":"BarringtonPD 1","tg_group":"Barrington","frequency":null,"metadata":{"encrypted":true},"tags":["Law Dispatch"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"8ced323c-b76f-4d3f-ac47-6c9fb03fc922","system_id":197,"tgid":1716,"name":"Police Secondary","alpha_tag":"BarringtonPD 2","tg_group":"Barrington","frequency":null,"metadata":null,"tags":["Law Tac"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"8fbae88f-d580-463a-8454-b68312207f0a","system_id":197,"tgid":1744,"name":"Fire Operations (Patch from VHF)","alpha_tag":"Bristol FD","tg_group":"Bristol","frequency":null,"metadata":null,"tags":["Fire Dispatch"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"3b584c62-3164-42b4-9753-b5d5027ce15a","system_id":197,"tgid":1755,"name":"Harbormaster","alpha_tag":"Bristol Harbor","tg_group":"Bristol","frequency":null,"metadata":null,"tags":["Public Works"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"4f0a5825-0d8f-450e-b7f2-bf4f0152e5d4","system_id":197,"tgid":2003,"name":"Police","alpha_tag":"Burrville PD","tg_group":"Burrillville","frequency":null,"metadata":null,"tags":["Law Dispatch"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"9435807f-9d4b-47be-afb7-7970466a5626","system_id":197,"tgid":2004,"name":"Police 2","alpha_tag":"Burrvl PD2","tg_group":"Burrillville","frequency":null,"metadata":null,"tags":["Law Talk"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"fe33408c-f9e8-4e2c-b974-08a32d29416b","system_id":197,"tgid":2005,"name":"Police 3 Detectives","alpha_tag":"Burrvl PD3","tg_group":"Burrillville","frequency":null,"metadata":{"encrypted":true},"tags":["Law Tac"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"af206a32-9cff-4d4a-b5e4-98320982c85a","system_id":197,"tgid":2006,"name":"Police 4","alpha_tag":"Burrvl PD4","tg_group":"Burrillville","frequency":null,"metadata":null,"tags":["Law Tac"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"ad703848-59c0-4a4b-93a1-d5b2f5bfef5a","system_id":197,"tgid":2000,"name":"Fire Misc (Ops are VHF)","alpha_tag":"Burrvl FD","tg_group":"Burrillville","frequency":null,"metadata":null,"tags":["Fire-Tac"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"6ed92da4-82ca-4956-8e5b-6fe9d8a9ddd9","system_id":197,"tgid":2001,"name":"Fire TAC-1","alpha_tag":"Burvl FDTAC1","tg_group":"Burrillville","frequency":null,"metadata":null,"tags":["Fire-Tac"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"eb09277b-92ce-4904-aefa-18500944cbe8","system_id":197,"tgid":2009,"name":"Fire TAC-2","alpha_tag":"Burvl FDTAC2","tg_group":"Burrillville","frequency":null,"metadata":null,"tags":["Fire-Tac"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"00a0b152-7ea6-4729-a861-d2f6497a3235","system_id":197,"tgid":2002,"name":"EMS Misc (Ops are VHF)","alpha_tag":"Burrvl EMS","tg_group":"Burrillville","frequency":null,"metadata":null,"tags":["EMS-Tac"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"c37f4192-779e-41d9-ab3b-1c5424fce0b7","system_id":197,"tgid":2007,"name":"Town-Wide","alpha_tag":"Burrvl Town","tg_group":"Burrillville","frequency":null,"metadata":null,"tags":["Multi-Tac"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"27b03072-e641-4a76-9f03-abaa40abc944","system_id":197,"tgid":2008,"name":"Emergency Management","alpha_tag":"Burrvl EMA","tg_group":"Burrillville","frequency":null,"metadata":null,"tags":["Emergency Ops"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"8fddeb21-91d9-45c0-8767-af847afd0edb","system_id":197,"tgid":1838,"name":"Police 1 Dispatch","alpha_tag":"CentFallsPD1","tg_group":"Central Falls","frequency":null,"metadata":null,"tags":["Law Dispatch"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"5d8857b7-99ac-418e-8aff-abe3037ffe7f","system_id":197,"tgid":1839,"name":"Police 2","alpha_tag":"CentFallsPD2","tg_group":"Central Falls","frequency":null,"metadata":null,"tags":["Law Dispatch"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"a68aa8af-5e39-4c41-ae73-4d373c5ebebc","system_id":197,"tgid":1835,"name":"Fire Dispatch (Simulcast of UHF)","alpha_tag":"CentFalls FD 1","tg_group":"Central Falls","frequency":null,"metadata":null,"tags":["Fire Dispatch"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"9cdcc595-bcce-4c7b-93d8-df93fab7e125","system_id":197,"tgid":1836,"name":"Fireground","alpha_tag":"CentFalls FD 2","tg_group":"Central Falls","frequency":null,"metadata":null,"tags":["Fire-Tac"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"ddebafe6-5a31-4d5d-81e2-d2ce9c2b1789","system_id":197,"tgid":1425,"name":"Police Operations - Simulcast of UHF","alpha_tag":"CharlestownPD","tg_group":"Charlestown","frequency":null,"metadata":null,"tags":["Law Dispatch"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"2f0fea19-31a2-4022-8777-a93143dfdcbf","system_id":197,"tgid":1429,"name":"EMS - Linked to 151.3325","alpha_tag":"Chastown EMS","tg_group":"Charlestown","frequency":null,"metadata":null,"tags":["EMS Dispatch"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"a68406e8-7707-4ff0-8834-e197a4034aa4","system_id":197,"tgid":1483,"name":"Police 1 - Dispatch","alpha_tag":"Coventry PD","tg_group":"Coventry","frequency":null,"metadata":null,"tags":["Law Dispatch"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"8afa3f85-b8a6-4708-8aeb-bac880b5b89b","system_id":197,"tgid":1484,"name":"Police 2","alpha_tag":"Coventry PD2","tg_group":"Coventry","frequency":null,"metadata":null,"tags":["Law Tac"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"93da5381-0164-4021-84e6-48b6226a1b78","system_id":197,"tgid":1480,"name":"Fire","alpha_tag":"Coventry FD","tg_group":"Coventry","frequency":null,"metadata":null,"tags":["Fire Dispatch"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"021851f5-d9ac-4f31-ba89-ddfc454c5f8f","system_id":197,"tgid":1500,"name":"Fire - Dispatch/Operations","alpha_tag":"Cranston FD Disp","tg_group":"Cranston","frequency":null,"metadata":null,"tags":["Fire Dispatch"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"72ac89b3-8b19-4537-84c1-9e9beac03c87","system_id":197,"tgid":1501,"name":"Fire - Fireground 2","alpha_tag":"Cranston FD FG2","tg_group":"Cranston","frequency":null,"metadata":null,"tags":["Fire-Tac"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"5a27db02-9de3-4ae3-ba42-318813487685","system_id":197,"tgid":1502,"name":"Fire - Fireground 3","alpha_tag":"Cranston FD FG3","tg_group":"Cranston","frequency":null,"metadata":null,"tags":["Fire-Tac"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"929359ca-8c5e-494e-952d-c1af42ea3d16","system_id":197,"tgid":1503,"name":"Fire - Fireground 4","alpha_tag":"Cranston FD FG4","tg_group":"Cranston","frequency":null,"metadata":null,"tags":["Fire-Talk"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"76c1bdd1-9ab8-4292-9c6d-aee4de5ef9f9","system_id":197,"tgid":1504,"name":"Fire - Admin/Alt Fireground 5","alpha_tag":"Cranston FD Admi","tg_group":"Cranston","frequency":null,"metadata":null,"tags":["Fire-Talk"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"dcf08dfc-bd02-4808-8939-8585928a0f7d","system_id":197,"tgid":1520,"name":"Fire","alpha_tag":"Cumberland FD","tg_group":"Cumberland","frequency":null,"metadata":null,"tags":["Fire Dispatch"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"e50be1a6-dc1d-4768-a853-7988fddce562","system_id":197,"tgid":1523,"name":"Police Secondary","alpha_tag":"Cumberland PD","tg_group":"Cumberland","frequency":null,"metadata":null,"tags":["Law Dispatch"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"e9b948c9-18bb-43e9-b3e5-c400cde5e60c","system_id":197,"tgid":1776,"name":"Fire Talk Around","alpha_tag":"E Greenwich F-TA","tg_group":"East Greenwich","frequency":null,"metadata":null,"tags":["Fire-Talk"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"5ead6fc7-ae77-4a1d-a59b-188a4b21c86f","system_id":197,"tgid":1779,"name":"Police Operations","alpha_tag":"E Greenwich PD","tg_group":"East Greenwich","frequency":null,"metadata":null,"tags":["Law Dispatch"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"bc23d728-b453-47ea-9a65-0af24c56d080","system_id":197,"tgid":1869,"name":"Police 1 - Dispatch","alpha_tag":"E Prov PD 1","tg_group":"East Providence","frequency":null,"metadata":null,"tags":["Law Dispatch"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"0a869133-2088-4805-bd55-c446e25eb075","system_id":197,"tgid":1872,"name":"Police 2","alpha_tag":"E Prov PD 2","tg_group":"East Providence","frequency":null,"metadata":{"encrypted":true},"tags":["Law Talk"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"90bafccc-bec6-4775-b640-1d9a2b7f512b","system_id":197,"tgid":1870,"name":"Police 3","alpha_tag":"E Prov PD 3","tg_group":"East Providence","frequency":null,"metadata":{"encrypted":true},"tags":["Law Talk"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"54bfc9d0-0532-4df5-aaa7-c3a96bc59b48","system_id":197,"tgid":1883,"name":"Detectives","alpha_tag":"E Prov PD12","tg_group":"East Providence","frequency":null,"metadata":{"encrypted":true},"tags":["Law Talk"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"9f77d904-2c5b-4e26-b163-defde5ee6a0f","system_id":197,"tgid":1866,"name":"Fire - Dispatch/Operations","alpha_tag":"E Prov FD 1","tg_group":"East Providence","frequency":null,"metadata":null,"tags":["Fire Dispatch"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"bb3e9346-cef8-4f0a-a951-5ef30fa47a36","system_id":197,"tgid":1867,"name":"Fire \"Channel 2\"","alpha_tag":"E Prov FD 2","tg_group":"East Providence","frequency":null,"metadata":null,"tags":["Fire-Tac"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"4e75aea9-e111-4596-a685-a591121966e0","system_id":197,"tgid":1878,"name":"Fire \"Channel 3\"","alpha_tag":"E Prov FD 3","tg_group":"East Providence","frequency":null,"metadata":null,"tags":["Fire-Tac"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"31650d51-0354-4a84-9580-ff560760fd36","system_id":197,"tgid":2064,"name":"Fire - Fireground","alpha_tag":"Exeter FD-G","tg_group":"Exeter","frequency":null,"metadata":null,"tags":["Fire-Tac"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"514ca197-c875-41d0-ad92-16eba7627e23","system_id":197,"tgid":1904,"name":"Fire","alpha_tag":"Foster Fire","tg_group":"Foster","frequency":null,"metadata":null,"tags":["Fire Dispatch"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"98322eb5-cf43-472b-92e5-b887d4630fb8","system_id":197,"tgid":1939,"name":"Police","alpha_tag":"Glocester PD","tg_group":"Glocester","frequency":null,"metadata":null,"tags":["Law Dispatch"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"d4747ead-6eb8-4acd-9c5b-078143ee26a5","system_id":197,"tgid":1940,"name":"Police Secondary","alpha_tag":"Glocester PD 2","tg_group":"Glocester","frequency":null,"metadata":null,"tags":["Law Tac"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"86ad2313-9d50-4172-b470-bf24a865837c","system_id":197,"tgid":1410,"name":"Police","alpha_tag":"Hopkinton PD","tg_group":"Hopkinton","frequency":null,"metadata":{"encrypted":true},"tags":["Law Dispatch"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"9123461c-41f5-4f99-aa99-ce24eb4d7885","system_id":197,"tgid":1100,"name":"Police 1 - Dispatch","alpha_tag":"Jamestown PD 1","tg_group":"Jamestown","frequency":null,"metadata":{"encrypted":true},"tags":["Law Dispatch"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"76e3336e-6549-4622-958f-df297b9fa007","system_id":197,"tgid":1101,"name":"Police 2","alpha_tag":"Jamestown PD 2","tg_group":"Jamestown","frequency":null,"metadata":{"encrypted":true},"tags":["Law Dispatch"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"864bafd7-cd4c-41b2-bb57-66ab431a032b","system_id":197,"tgid":1108,"name":"Fire","alpha_tag":"Jamestown FD","tg_group":"Jamestown","frequency":null,"metadata":null,"tags":["Fire Dispatch"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"72b9a7e9-37ed-448d-8801-f29055d3090d","system_id":197,"tgid":1120,"name":"Fireground 1","alpha_tag":"Jamestown FG 1","tg_group":"Jamestown","frequency":null,"metadata":null,"tags":["Fire-Tac"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"24637182-54f9-4424-83c7-b98b938045da","system_id":197,"tgid":1121,"name":"Fireground 2","alpha_tag":"Jamestown FG 2","tg_group":"Jamestown","frequency":null,"metadata":null,"tags":["Fire-Tac"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"51984385-4b0e-43f7-ba95-1a493f321f09","system_id":197,"tgid":1114,"name":"Public Works","alpha_tag":"Jamestown DPW","tg_group":"Jamestown","frequency":null,"metadata":null,"tags":["Public Works"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"66603022-c1df-4579-b99e-d9d20d573ad5","system_id":197,"tgid":1107,"name":"Town Schools","alpha_tag":"Jamestown School","tg_group":"Jamestown","frequency":null,"metadata":null,"tags":["Schools"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"3171c8fe-f7f1-44e4-a13b-b365b2ebb44f","system_id":197,"tgid":1619,"name":"Police Operations","alpha_tag":"Johnston PD","tg_group":"Johnston","frequency":null,"metadata":{"encrypted":true},"tags":["Law Dispatch"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"0ffb6907-1363-45cd-8838-f0bdd4c812f0","system_id":197,"tgid":1616,"name":"Fire Operations","alpha_tag":"Johnston FD","tg_group":"Johnston","frequency":null,"metadata":null,"tags":["Fire Dispatch"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"42577410-aca0-48c2-afbc-4c79c62572e2","system_id":197,"tgid":1617,"name":"Fireground","alpha_tag":"Johnston FG","tg_group":"Johnston","frequency":null,"metadata":null,"tags":["Fire-Tac"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"0f8ed94e-e62b-4de7-aa1c-c84c887e1f7c","system_id":197,"tgid":1683,"name":"Police F1","alpha_tag":"Lincoln Police","tg_group":"Lincoln","frequency":null,"metadata":null,"tags":["Law Dispatch"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"31e927df-e52a-4f8f-8662-7eb5d3a4fe16","system_id":197,"tgid":1684,"name":"Police F2","alpha_tag":"Lincoln Police 2","tg_group":"Lincoln","frequency":null,"metadata":null,"tags":["Law Tac"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"fafce236-23e1-46c9-9fff-7fbaff4ffe94","system_id":197,"tgid":1680,"name":"Fire Dispatch","alpha_tag":"Lincoln Fire 1","tg_group":"Lincoln","frequency":null,"metadata":null,"tags":["Fire Dispatch"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"f4589733-e563-419d-b045-aad3e226488a","system_id":197,"tgid":1681,"name":"Fireground 2","alpha_tag":"Lincoln Fire 2","tg_group":"Lincoln","frequency":null,"metadata":null,"tags":["Fire-Tac"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"c02cca42-91ae-4169-9ce5-039d6ab00e40","system_id":197,"tgid":1691,"name":"Fireground 3","alpha_tag":"Lincoln Fire 3","tg_group":"Lincoln","frequency":null,"metadata":null,"tags":["Fire-Tac"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"f67aab29-332d-4144-8b35-507c7c8a09c4","system_id":197,"tgid":1682,"name":"EMS","alpha_tag":"Lincoln EMS","tg_group":"Lincoln","frequency":null,"metadata":null,"tags":["EMS Dispatch"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"db07105d-c310-4362-8405-da3b2169f5a9","system_id":197,"tgid":1688,"name":"Emergency Management","alpha_tag":"Lincoln EMA","tg_group":"Lincoln","frequency":null,"metadata":null,"tags":["Emergency Ops"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"10c9d009-6e5e-4ef1-b570-680746acd0cc","system_id":197,"tgid":1687,"name":"Townwide","alpha_tag":"Lincoln Townwide","tg_group":"Lincoln","frequency":null,"metadata":null,"tags":["Interop"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"7760331b-6631-48d6-9342-b051b5df4106","system_id":197,"tgid":1692,"name":"Public Works","alpha_tag":"Lincoln DPW","tg_group":"Lincoln","frequency":null,"metadata":null,"tags":["Public Works"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"37cf7aee-9b0c-4c10-a8f9-980630f34ce0","system_id":197,"tgid":1264,"name":"Police","alpha_tag":"LittleCompPD","tg_group":"Little Compton","frequency":null,"metadata":null,"tags":["Law Dispatch"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"01c0ab7a-c65e-402d-b9b2-16cbc50e73a3","system_id":197,"tgid":1266,"name":"Fire","alpha_tag":"LittleCompFD","tg_group":"Little Compton","frequency":null,"metadata":null,"tags":["Fire Dispatch"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"2eaf9364-01e2-406b-98b8-2c30d346bc4b","system_id":197,"tgid":1338,"name":"Police Operations","alpha_tag":"MiddletownPD","tg_group":"Middletown","frequency":null,"metadata":null,"tags":["Law Dispatch"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"2fa319f2-45a8-457e-8122-eaf4ad5425c2","system_id":197,"tgid":1343,"name":"Fire Operations","alpha_tag":"Middletown FD","tg_group":"Middletown","frequency":null,"metadata":null,"tags":["Fire Dispatch"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"49ee160e-17b9-4541-82ae-e5df820ac85d","system_id":197,"tgid":1345,"name":"Townwide","alpha_tag":"MiddletownTW","tg_group":"Middletown","frequency":null,"metadata":null,"tags":["Multi-Dispatch"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"e3f8e784-870f-487a-b6cc-0d163833df63","system_id":197,"tgid":1001,"name":"Police - Dispatch","alpha_tag":"Narrag PD 1","tg_group":"Narragansett","frequency":null,"metadata":{"encrypted":true},"tags":["Law Dispatch"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"6613a9cc-9474-47b6-9928-35b9f6f4f8c0","system_id":197,"tgid":1002,"name":"Police - Car/Car","alpha_tag":"Narrag PD 2","tg_group":"Narragansett","frequency":null,"metadata":{"encrypted":true},"tags":["Law Talk"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"e70dbeeb-ae7b-44cd-b9bc-41033aa5baf4","system_id":197,"tgid":1003,"name":"Police - Special Details 1/Town Beaches","alpha_tag":"Narrag PD 3","tg_group":"Narragansett","frequency":null,"metadata":{"encrypted":true},"tags":["Law Tac"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"0d45e24d-72ea-44a2-8e3c-a030c9937ab8","system_id":197,"tgid":1004,"name":"Police - Special Details 2","alpha_tag":"Narrag PD 4","tg_group":"Narragansett","frequency":null,"metadata":{"encrypted":true},"tags":["Law Tac"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"409a7cbf-05ae-41f9-b425-254543d94d11","system_id":197,"tgid":1005,"name":"Police - Harbormaster","alpha_tag":"Narrag PD 5","tg_group":"Narragansett","frequency":null,"metadata":{"encrypted":true},"tags":["Law Talk"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"5900b90a-e703-497d-9856-d2441d14ba49","system_id":197,"tgid":1007,"name":"Police - Detectives","alpha_tag":"Narrag PD 7","tg_group":"Narragansett","frequency":null,"metadata":{"encrypted":true},"tags":["Law Talk"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"a677de8b-18cb-454b-99dd-d9daa7ccbb75","system_id":197,"tgid":1008,"name":"Police - Detectives","alpha_tag":"Narrag PD 8","tg_group":"Narragansett","frequency":null,"metadata":{"encrypted":true},"tags":["Law Talk"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"00dae4e2-e5df-4cf3-859e-bddada6745fb","system_id":197,"tgid":1006,"name":"Fire - Dispatch","alpha_tag":"Narrag FD","tg_group":"Narragansett","frequency":null,"metadata":null,"tags":["Fire Dispatch"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"a6a04c5c-37c7-4a35-836f-11732ce8bc27","system_id":197,"tgid":1012,"name":"Fire - Fireground 1","alpha_tag":"Narrag FDFG1","tg_group":"Narragansett","frequency":null,"metadata":null,"tags":["Fire-Tac"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"b4886861-1fc7-4c82-a491-bfabd7a19df5","system_id":197,"tgid":1013,"name":"Fire - Fireground 2","alpha_tag":"Narrag FDFG2","tg_group":"Narragansett","frequency":null,"metadata":null,"tags":["Fire-Tac"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"0fdc78a5-5dbb-42fd-b7f9-296566557fab","system_id":197,"tgid":1016,"name":"Fire - Administration","alpha_tag":"Narrag FD AD","tg_group":"Narragansett","frequency":null,"metadata":null,"tags":["Fire-Talk"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"885b039f-30e7-46f0-8d59-61e19b642221","system_id":197,"tgid":1014,"name":"Fire - EMS Ops","alpha_tag":"Narrag EMS","tg_group":"Narragansett","frequency":null,"metadata":null,"tags":["EMS Dispatch"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"db44a694-97b8-4d99-808f-e1e037c68bf7","system_id":197,"tgid":1017,"name":"Public Works","alpha_tag":"Narrag DPW","tg_group":"Narragansett","frequency":null,"metadata":null,"tags":["Public Works"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"c5e5de1d-2c68-4923-88ec-1189fb2e3697","system_id":197,"tgid":1010,"name":"Town Administration","alpha_tag":"Narrag TownA","tg_group":"Narragansett","frequency":null,"metadata":null,"tags":["Other"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"3cef09ff-14be-4392-a801-f6eaee414091","system_id":197,"tgid":1011,"name":"Townwide Interop","alpha_tag":"Narrag IOP","tg_group":"Narragansett","frequency":null,"metadata":null,"tags":["Interop"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"58b45f2d-ec82-417c-aaba-160cd640ff73","system_id":197,"tgid":1376,"name":"Police","alpha_tag":"New Shore PD","tg_group":"New Shoreham","frequency":null,"metadata":null,"tags":["Law Dispatch"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"495fe4a0-5ce1-402c-a728-7ed3235b95e6","system_id":197,"tgid":1300,"name":"Police 1 - Dispatch","alpha_tag":"Newport PD 1","tg_group":"Newport","frequency":null,"metadata":{"encrypted":true},"tags":["Law Dispatch"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"9f571fa5-e656-4aa5-9fae-1ebdd7aa6269","system_id":197,"tgid":1302,"name":"Police 2 - Records","alpha_tag":"Newport PD 2","tg_group":"Newport","frequency":null,"metadata":{"encrypted":true},"tags":["Law Talk"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"c2ec7f40-57b3-4593-bc84-888c970fd528","system_id":197,"tgid":1304,"name":"Police 4 - Tactical 1","alpha_tag":"Newport PD 4","tg_group":"Newport","frequency":null,"metadata":{"encrypted":true},"tags":["Law Talk"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"d4a99a1e-ab9d-4420-9345-37cd6d02282e","system_id":197,"tgid":1307,"name":"Police 7 - Tactical 4","alpha_tag":"Newport PD 7","tg_group":"Newport","frequency":null,"metadata":{"encrypted":true},"tags":["Law Talk"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"0981e140-232a-4a87-b83a-21d1845c408a","system_id":197,"tgid":1308,"name":"Police 8 - Tactical 5","alpha_tag":"Newport PD 8","tg_group":"Newport","frequency":null,"metadata":{"encrypted":true},"tags":["Law Talk"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"d7570438-1303-4a0b-95a3-0dcca6e3aa2d","system_id":197,"tgid":1303,"name":"Fire Dispatch/Operations","alpha_tag":"Newport FD1","tg_group":"Newport","frequency":null,"metadata":null,"tags":["Fire Dispatch"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"f04715d8-7927-4a96-879a-4f3690ac2025","system_id":197,"tgid":1305,"name":"Fireground Ops 1","alpha_tag":"Newport FG1","tg_group":"Newport","frequency":null,"metadata":null,"tags":["Fire-Tac"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"a60c7db1-5e05-41eb-834b-734355fe4a05","system_id":197,"tgid":1306,"name":"Fireground Ops 2","alpha_tag":"Newport FG2","tg_group":"Newport","frequency":null,"metadata":null,"tags":["Fire-Tac"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"9bd3899d-920e-45f1-846d-432f9b08e64d","system_id":197,"tgid":1301,"name":"Fire - Training","alpha_tag":"Newport FDT","tg_group":"Newport","frequency":null,"metadata":null,"tags":["Fire-Talk"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"7f9b3896-5d5a-47a7-ac18-3c3833e1a342","system_id":197,"tgid":1291,"name":"Water Department","alpha_tag":"Newport Water","tg_group":"Newport","frequency":null,"metadata":null,"tags":["Public Works"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"5ead69d4-f975-412f-91a4-9ed832f69e6e","system_id":197,"tgid":1293,"name":"Public Works","alpha_tag":"Newport DPW","tg_group":"Newport","frequency":null,"metadata":null,"tags":["Public Works"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"9c63b453-ec04-4c9e-ba5c-f944232d1035","system_id":197,"tgid":1297,"name":"Citywide Events","alpha_tag":"Newport Evnt","tg_group":"Newport","frequency":null,"metadata":null,"tags":["Public Works"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"3f64434a-bae0-40f6-906a-d3fdb1f4415b","system_id":197,"tgid":1312,"name":"Newport Citywide","alpha_tag":"Newport CW","tg_group":"Newport","frequency":null,"metadata":null,"tags":["Interop"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"0af9ce8c-208b-420e-a526-741539fa3203","system_id":197,"tgid":1285,"name":"Police 1 - Dispatch","alpha_tag":"NKing PD 1","tg_group":"North Kingstown","frequency":null,"metadata":null,"tags":["Law Dispatch"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"c77ecba4-10fd-4718-b227-e0b430f9bcb0","system_id":197,"tgid":1286,"name":"Police 2 - Admin","alpha_tag":"NKing PD 2","tg_group":"North Kingstown","frequency":null,"metadata":{"encrypted":true},"tags":["Law Talk"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"49a3d385-40dc-4229-a912-0ce80f2007cd","system_id":197,"tgid":1287,"name":"Police 3 - Car/Car","alpha_tag":"NKing PD 3","tg_group":"North Kingstown","frequency":null,"metadata":null,"tags":["Law Tac"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"42a708a7-21aa-4998-bb45-d4e428811984","system_id":197,"tgid":1280,"name":"Fire - Dispatch","alpha_tag":"NKing Fire D","tg_group":"North Kingstown","frequency":null,"metadata":null,"tags":["Fire Dispatch"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"ecad349c-c35d-4935-95ce-fe0b002cee5e","system_id":197,"tgid":1281,"name":"Fire - Fireground","alpha_tag":"NKing Fire G","tg_group":"North Kingstown","frequency":null,"metadata":null,"tags":["Fire-Tac"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"71c47935-e281-4bfc-8b8b-652b69ccb092","system_id":197,"tgid":1536,"name":"Police 1 - Dispatch","alpha_tag":"NorthPrv PD1","tg_group":"North Providence","frequency":null,"metadata":{"encrypted":true},"tags":["Law Dispatch"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"e55a20f1-b9f9-4d04-a296-124621928739","system_id":197,"tgid":1537,"name":"Police 2 - Car/Car","alpha_tag":"NorthPrv PD2","tg_group":"North Providence","frequency":null,"metadata":{"encrypted":true},"tags":["Law Talk"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"a86671cc-1801-42b9-93e3-bf9d19f825c3","system_id":197,"tgid":1538,"name":"Police 3 - Tactical","alpha_tag":"NorthPrv PD3","tg_group":"North Providence","frequency":null,"metadata":{"encrypted":true},"tags":["Law Tac"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"dd54ae16-88e4-4efb-9efe-65dcdad34bc8","system_id":197,"tgid":1547,"name":"Fire Dispatch ","alpha_tag":"NorthPrv FDD","tg_group":"North Providence","frequency":null,"metadata":null,"tags":["Fire Dispatch"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"60010e7c-8c99-4cd5-b9e3-20ca7d39d4ba","system_id":197,"tgid":1548,"name":"Fire 2","alpha_tag":"NorthPrv Fire 2","tg_group":"North Providence","frequency":null,"metadata":null,"tags":["Fire-Tac"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"801a175b-1c76-4057-832f-3f36d7d893e2","system_id":197,"tgid":1549,"name":"Fire 3","alpha_tag":"NorthPrv Fire 3","tg_group":"North Providence","frequency":null,"metadata":null,"tags":["Fire-Tac"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"16e4c7bb-db54-4d0b-a484-49330027368b","system_id":197,"tgid":1550,"name":"Fire 4","alpha_tag":"NorthPrv Fire 4","tg_group":"North Providence","frequency":null,"metadata":null,"tags":["Fire-Tac"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"34f9c697-76b4-4915-b2da-1c5be68ef4ee","system_id":197,"tgid":1551,"name":"Fire 5","alpha_tag":"NorthPrv Fire 5","tg_group":"North Providence","frequency":null,"metadata":null,"tags":["Fire Dispatch"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"be8cb8fa-7dc5-483f-b70c-2c896334cb1f","system_id":197,"tgid":1552,"name":"Fire 6","alpha_tag":"NorthPrv Fire 6","tg_group":"North Providence","frequency":null,"metadata":{"encrypted":true},"tags":["Fire-Tac"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"9cb5dfe0-44fa-4861-97ff-5dfd02f2ba38","system_id":197,"tgid":1544,"name":"Townwide 1","alpha_tag":"NorthPrv TownW 1","tg_group":"North Providence","frequency":null,"metadata":null,"tags":["Interop"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"84c53dd7-18c8-460d-a743-a8e9d4aeae20","system_id":197,"tgid":1545,"name":"Townwide 2","alpha_tag":"NorthPrv TownW 2","tg_group":"North Providence","frequency":null,"metadata":null,"tags":["Interop"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"ccef002d-82ca-4525-92b8-d8f2a8df3b0c","system_id":197,"tgid":1554,"name":"Public Works","alpha_tag":"NorthPrv DPW","tg_group":"North Providence","frequency":null,"metadata":null,"tags":["Public Works"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"35f15b9b-370d-4a80-94ca-8e9a133eb520","system_id":197,"tgid":1971,"name":"Police","alpha_tag":"N Smithfd PD","tg_group":"North Smithfield","frequency":null,"metadata":{"encrypted":true},"tags":["Law Dispatch"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"94f2dd5c-0873-4f52-b15d-828846e37df6","system_id":197,"tgid":1968,"name":"Fire Dispatch/Operations","alpha_tag":"N Smithfield FD","tg_group":"North Smithfield","frequency":null,"metadata":null,"tags":["Fire Dispatch"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"8fd10658-b480-42ac-8423-3633957e688e","system_id":197,"tgid":1969,"name":"Fire Secondary","alpha_tag":"N Smithfield FD2","tg_group":"North Smithfield","frequency":null,"metadata":null,"tags":["Fire-Tac"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"924ffe37-13b5-4c76-bd8a-56da8bb07daa","system_id":197,"tgid":1981,"name":"Fireground","alpha_tag":"N Smithfield FD3","tg_group":"North Smithfield","frequency":null,"metadata":null,"tags":["Fire-Tac"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"8eb4eb8f-7334-4992-96e2-766a4109150e","system_id":197,"tgid":1440,"name":"Fire - Operations","alpha_tag":"Pawtucket FD 1","tg_group":"Pawtucket","frequency":null,"metadata":null,"tags":["Fire Dispatch"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"ed424f0f-7435-43cd-aa66-e5baaa03edc9","system_id":197,"tgid":1441,"name":"Fireground","alpha_tag":"Pawtucket FG","tg_group":"Pawtucket","frequency":null,"metadata":null,"tags":["Fire-Tac"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"18e8305b-b19f-40c6-b4dd-b4aa3886cb50","system_id":197,"tgid":1442,"name":"EMS Tac","alpha_tag":"Pawtucket EMSTac","tg_group":"Pawtucket","frequency":null,"metadata":null,"tags":["EMS-Tac"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"90940fc6-d4ca-4e21-9380-9e4ed60a0e2a","system_id":197,"tgid":1248,"name":"Police","alpha_tag":"PortsmouthPD","tg_group":"Portsmouth","frequency":null,"metadata":{"encrypted":true},"tags":["Law Dispatch"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"f07f1b2a-6bb5-4601-ba57-8a27cbdc20a1","system_id":197,"tgid":1253,"name":"Fire Dispatch (Patch to VHF Primary)","alpha_tag":"Portsmouth FD","tg_group":"Portsmouth","frequency":null,"metadata":null,"tags":["Fire Dispatch"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"759f76b0-889a-43ce-a5ce-3ca91a4eb5c2","system_id":197,"tgid":1255,"name":"Fireground","alpha_tag":"Portsmouth FG","tg_group":"Portsmouth","frequency":null,"metadata":null,"tags":["Fire-Tac"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"f8580819-da04-402c-8177-0c01746de44f","system_id":197,"tgid":1262,"name":"Island Fire Dispatch","alpha_tag":"Prudence Isl FD","tg_group":"Portsmouth","frequency":null,"metadata":null,"tags":["Fire Dispatch"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"3db6e340-2e78-43db-b635-516e87b33e4b","system_id":197,"tgid":10000,"name":"Police - All Call - Emergency Broadcasts","alpha_tag":"PPD ATG","tg_group":"Providence (City)","frequency":null,"metadata":null,"tags":["Emergency Ops"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"412ba3df-6854-4920-b5ea-27ec09771095","system_id":197,"tgid":10001,"name":"Police 1 - Dispatch","alpha_tag":"PPD CH 1","tg_group":"Providence (City)","frequency":null,"metadata":null,"tags":["Law Dispatch"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"4f42158b-dba6-4d48-94c0-64b411253867","system_id":197,"tgid":10002,"name":"Police 2","alpha_tag":"PPD CH 2","tg_group":"Providence (City)","frequency":null,"metadata":{"encrypted":true},"tags":["Law Talk"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"6095467c-89ba-48e6-a543-758d7093a494","system_id":197,"tgid":10003,"name":"Police 3","alpha_tag":"PPD CH 3","tg_group":"Providence (City)","frequency":null,"metadata":{"encrypted":true},"tags":["Law Talk"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"df5cc36d-09c7-4647-aa41-f29c380a987b","system_id":197,"tgid":10004,"name":"Police 4","alpha_tag":"PPD CH-4","tg_group":"Providence (City)","frequency":null,"metadata":{"encrypted":true},"tags":["Law Talk"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"1ecdcf84-765f-4e5d-bcee-fc1c02181f57","system_id":197,"tgid":10005,"name":"Police 5 -Detectives 1","alpha_tag":"PPD DETEC 1","tg_group":"Providence (City)","frequency":null,"metadata":{"encrypted":true},"tags":["Law Talk"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"0f44fcd6-29f0-4dc1-af53-c9ae0d8869fe","system_id":197,"tgid":10006,"name":"Police 6 - Car-to-Car","alpha_tag":"PPD T/A","tg_group":"Providence (City)","frequency":null,"metadata":{"encrypted":true},"tags":["Law Talk"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"67fdc7a2-c67b-425f-93c5-be8d9f630c1d","system_id":197,"tgid":10007,"name":"Police 7 - Narcotics 1","alpha_tag":"PPD NARC 1","tg_group":"Providence (City)","frequency":null,"metadata":{"encrypted":true},"tags":["Law Talk"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"063c02fd-75cf-44c1-aec9-d2e2ef6e6431","system_id":197,"tgid":10008,"name":"Police 8 - Narcotics 2","alpha_tag":"PPD NARC 2","tg_group":"Providence (City)","frequency":null,"metadata":{"encrypted":true},"tags":["Law Tac"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"d5f5ad04-8907-4dc6-9f46-494dccf403da","system_id":197,"tgid":10009,"name":"Police 9 - Detectives 2","alpha_tag":"PPD DETEC 2","tg_group":"Providence (City)","frequency":null,"metadata":{"encrypted":true},"tags":["Law Talk"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"d7f09417-0d2c-4e29-8198-b0f341e284c4","system_id":197,"tgid":10010,"name":"Police 10 - Special Details 1","alpha_tag":"PPD DETAIL 1","tg_group":"Providence (City)","frequency":null,"metadata":{"encrypted":true},"tags":["Law Tac"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"be8fa60c-1a47-4d6b-955d-d2c04dad86d2","system_id":197,"tgid":10011,"name":"Police 11 - Special Details 2","alpha_tag":"PPD DETAIL 2","tg_group":"Providence (City)","frequency":null,"metadata":{"encrypted":true},"tags":["Law Tac"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"053d5d25-b014-43d8-b643-22cdcb5004fa","system_id":197,"tgid":10012,"name":"Police 12 - Corrections Security","alpha_tag":"PPD CORR SEC","tg_group":"Providence (City)","frequency":null,"metadata":{"encrypted":true},"tags":["Law Talk"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"a46cfa2d-6ad2-4f93-bbc3-bd9a5a74660a","system_id":197,"tgid":10013,"name":"Police 13 - Special Response Unit","alpha_tag":"PPD SRU","tg_group":"Providence (City)","frequency":null,"metadata":{"encrypted":true},"tags":["Law Tac"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"f3d048a9-a436-44c0-a504-27d9a6219197","system_id":197,"tgid":10014,"name":"Police 14 - Administration","alpha_tag":"PPD ADMIN","tg_group":"Providence (City)","frequency":null,"metadata":{"encrypted":true},"tags":["Law Talk"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"a3f3633f-8417-43ba-bc27-f3619f387b6b","system_id":197,"tgid":10100,"name":"Fire All Call - Emergency Broadcasts","alpha_tag":"PROV FD ATG","tg_group":"Providence (City)","frequency":null,"metadata":null,"tags":["Emergency Ops"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"1a6cb9c1-dc22-4674-aa02-0724d137da2c","system_id":197,"tgid":10101,"name":"Fire Dispatch","alpha_tag":"PFD DISPATCH","tg_group":"Providence (City)","frequency":null,"metadata":null,"tags":["Fire Dispatch"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"b87b1615-d512-474f-a474-7dd1e17d02c9","system_id":197,"tgid":10107,"name":"Fireground 2","alpha_tag":"PFD CH-2 FG","tg_group":"Providence (City)","frequency":null,"metadata":null,"tags":["Fire-Tac"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"462a44fe-c150-4a3a-8f99-cc1e4953365e","system_id":197,"tgid":10108,"name":"Fireground 3","alpha_tag":"PFD CH-3 FG","tg_group":"Providence (City)","frequency":null,"metadata":null,"tags":["Fire-Tac"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"4299565e-1085-45b1-b62e-1d4ba18e17a5","system_id":197,"tgid":10109,"name":"Fireground 4","alpha_tag":"PFD CH-4 FG","tg_group":"Providence (City)","frequency":null,"metadata":null,"tags":["Fire-Tac"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"2164418b-fd1a-433f-bfb3-a126c860830a","system_id":197,"tgid":10102,"name":"Fire 5","alpha_tag":"PFD CH-5","tg_group":"Providence (City)","frequency":null,"metadata":null,"tags":["Fire-Tac"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"87293d92-71da-436e-8398-c1e37fb75c4b","system_id":197,"tgid":10103,"name":"Fire 6","alpha_tag":"PFD CH-6","tg_group":"Providence (City)","frequency":null,"metadata":null,"tags":["Fire-Tac"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"f02786e1-faf4-4610-8d13-77fbb9ae1806","system_id":197,"tgid":10104,"name":"Fire 7","alpha_tag":"PFD CH-7","tg_group":"Providence (City)","frequency":null,"metadata":null,"tags":["Fire-Tac"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"55a0abef-bad7-40c0-9473-469f1eca5a66","system_id":197,"tgid":10110,"name":"Fire - Mutual Aid 1","alpha_tag":"PFD M/A 1","tg_group":"Providence (City)","frequency":null,"metadata":null,"tags":["Fire-Tac"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"d53fa3dc-7cd3-47c3-b041-1d7e145f96eb","system_id":197,"tgid":10111,"name":"Fire - Mutual Aid 2","alpha_tag":"PFD M/A 2","tg_group":"Providence (City)","frequency":null,"metadata":null,"tags":["Fire-Tac"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"9654ab94-913d-4a50-ba50-f9e773842f4d","system_id":197,"tgid":10112,"name":"Fire - Mutual Aid 3","alpha_tag":"PFD M/A 3","tg_group":"Providence (City)","frequency":null,"metadata":null,"tags":["Fire-Tac"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"2a5faa60-869b-4365-8305-11f2ededd03e","system_id":197,"tgid":10113,"name":"Fireground 8","alpha_tag":"PFD Fireground 8","tg_group":"Providence (City)","frequency":null,"metadata":null,"tags":["Fire-Talk"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"0a73000e-db60-49a2-9a5f-5e194cf3b566","system_id":197,"tgid":10105,"name":"Fire - Administration","alpha_tag":"PFD ADMIN","tg_group":"Providence (City)","frequency":null,"metadata":null,"tags":["Fire-Talk"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"7a694690-3845-49d1-96f8-d2fd93b2aed5","system_id":197,"tgid":10106,"name":"Fire - Communications","alpha_tag":"PFD COMM","tg_group":"Providence (City)","frequency":null,"metadata":null,"tags":["Fire-Talk"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"5b7d44b5-b054-43f3-8e78-8e4fdf36e591","system_id":197,"tgid":10207,"name":"Public Works","alpha_tag":"PROV DPW","tg_group":"Providence (City)","frequency":null,"metadata":null,"tags":["Public Works"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"568c41d1-052c-4d0f-8b68-ca4c4bf5090d","system_id":197,"tgid":2035,"name":"Police","alpha_tag":"Richmond PD","tg_group":"Richmond","frequency":null,"metadata":null,"tags":["Law Dispatch"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"57df9db6-f0d9-4dd8-b11b-804f331adb7e","system_id":197,"tgid":2042,"name":"Chariho Regional High School","alpha_tag":"Chariho Reg HS","tg_group":"Richmond","frequency":null,"metadata":null,"tags":["Schools"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"fb087a56-04e9-422b-8d54-db40bcbc6e27","system_id":197,"tgid":1460,"name":"Police","alpha_tag":"Scituate PD","tg_group":"Scituate","frequency":null,"metadata":null,"tags":["Law Dispatch"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"2ff5eadd-fc14-4145-9e59-f0554c582513","system_id":197,"tgid":1463,"name":"Fire Operations","alpha_tag":"Scituate FD","tg_group":"Scituate","frequency":null,"metadata":null,"tags":["Fire Dispatch"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"42134a8d-aaef-4498-869b-a581ef1da251","system_id":197,"tgid":1651,"name":"Police Operations","alpha_tag":"SmithfieldPD","tg_group":"Smithfield","frequency":null,"metadata":null,"tags":["Law Dispatch"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"0be92843-487a-4eb8-911c-79a6f0195fc3","system_id":197,"tgid":1652,"name":"Police Secondary","alpha_tag":"Smfld PD 2","tg_group":"Smithfield","frequency":null,"metadata":null,"tags":["Law Tac"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"8ad6aee9-3c1d-42b5-897e-aa38ad8f47ab","system_id":197,"tgid":1653,"name":"Police Detectives","alpha_tag":"Smfld PD Det","tg_group":"Smithfield","frequency":null,"metadata":{"encrypted":true},"tags":["Law Tac"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"2fe0e3aa-3e6a-4cbf-94c1-6d468433185f","system_id":197,"tgid":1654,"name":"Police Admin","alpha_tag":"Smfld PD Adm","tg_group":"Smithfield","frequency":null,"metadata":{"encrypted":true},"tags":["Law Talk"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"c61c861b-96ca-45e3-8d31-f24d6f56ee85","system_id":197,"tgid":1661,"name":"Police Details","alpha_tag":"Smfld PD Dtl","tg_group":"Smithfield","frequency":null,"metadata":null,"tags":["Law Talk"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"092314a4-d765-4205-8153-22f1c97613c0","system_id":197,"tgid":1648,"name":"Fire - Fireground","alpha_tag":"SmithfieldFD","tg_group":"Smithfield","frequency":null,"metadata":null,"tags":["Fire-Tac"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"79eae292-ba96-4e10-91e7-00164e518b24","system_id":197,"tgid":1655,"name":"Town-Wide","alpha_tag":"Smfld Town","tg_group":"Smithfield","frequency":null,"metadata":null,"tags":["Multi-Tac"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"3f424c46-f9ea-43db-9c2c-34b512c403c1","system_id":197,"tgid":1657,"name":"Emergency Management","alpha_tag":"Smfld EMA","tg_group":"Smithfield","frequency":null,"metadata":null,"tags":["Emergency Ops"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"28ee1903-0a62-4651-bb80-5a072512a5e4","system_id":197,"tgid":1660,"name":"Public Works","alpha_tag":"Smfld DPW","tg_group":"Smithfield","frequency":null,"metadata":null,"tags":["Public Works"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"cd274b7f-d1fa-43f8-b005-8208ff1a063b","system_id":197,"tgid":1225,"name":"Police 1 - Dispatch","alpha_tag":"SKing PD 1","tg_group":"South Kingstown","frequency":null,"metadata":{"encrypted":true},"tags":["Law Dispatch"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"41039c74-036b-4b3d-a8b1-a0b93135a710","system_id":197,"tgid":1226,"name":"Police 2 - Car/Car","alpha_tag":"SKing PD 2","tg_group":"South Kingstown","frequency":null,"metadata":{"encrypted":true},"tags":["Law Talk"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"352da0f6-c312-43a0-9d1f-2329651bb3ab","system_id":197,"tgid":1235,"name":"Police 3 - Tactical","alpha_tag":"SKing PD 3","tg_group":"South Kingstown","frequency":null,"metadata":{"encrypted":true},"tags":["Law Tac"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"3984ab59-1f22-47e7-9cd4-4835e7a1a1b6","system_id":197,"tgid":1236,"name":"Police 5 - Tactical","alpha_tag":"SKing PD 5","tg_group":"South Kingstown","frequency":null,"metadata":{"encrypted":true},"tags":["Law Tac"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"6d8595f7-aef9-4f39-9141-7d2d31ea3599","system_id":197,"tgid":1232,"name":"Fire - UHF Simulcast","alpha_tag":"SKing FD Lnk","tg_group":"South Kingstown","frequency":null,"metadata":null,"tags":["Fire Dispatch"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"d405ff4b-5999-486f-92f3-259b452909b5","system_id":197,"tgid":1240,"name":"Fire - Detail","alpha_tag":"SKing Fire D","tg_group":"South Kingstown","frequency":null,"metadata":null,"tags":["Fire-Talk"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"7937d853-64d6-423d-ab4f-14e0d9fcee91","system_id":197,"tgid":1227,"name":"Union Fire District - Fireground 1","alpha_tag":"UnionFD FG 1","tg_group":"South Kingstown","frequency":null,"metadata":null,"tags":["Fire-Tac"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"84df5994-fdc1-4f04-9c02-5c8d561adb0e","system_id":197,"tgid":1237,"name":"Union Fire District - Fireground 2","alpha_tag":"UnionFD FG 2","tg_group":"South Kingstown","frequency":null,"metadata":null,"tags":["Fire-Tac"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"7dfd4748-fd4b-40f8-8e53-322471a410cd","system_id":197,"tgid":1026,"name":"Union Fire District - Special Events","alpha_tag":"UnionFD Evnt","tg_group":"South Kingstown","frequency":null,"metadata":null,"tags":["Fire-Talk"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"b3fd88e4-8b2e-4eb7-ae5d-ae994cb5eae3","system_id":197,"tgid":1015,"name":"EMS","alpha_tag":"SKing EMS","tg_group":"South Kingstown","frequency":null,"metadata":{"encrypted":true},"tags":["EMS Dispatch"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"eaf21cf9-005d-4560-96d2-2e4d9b97d7e9","system_id":197,"tgid":1316,"name":"Police (Simulcast 482.9625)","alpha_tag":"Tiverton PD","tg_group":"Tiverton","frequency":null,"metadata":null,"tags":["Law Dispatch"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"e488751a-fcd7-4aa1-b6c0-fcde9316f676","system_id":197,"tgid":1315,"name":"Fire (Simulcast 471.7875)","alpha_tag":"Tiverton FD","tg_group":"Tiverton","frequency":null,"metadata":null,"tags":["Fire Dispatch"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"fd527d9c-4210-4b85-9639-f09ea70533d2","system_id":197,"tgid":1162,"name":"Fire","alpha_tag":"Warwick FD","tg_group":"Warwick","frequency":null,"metadata":null,"tags":["Fire Dispatch"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"6fc60cbe-b4b7-4ed5-94fc-99177620b28c","system_id":197,"tgid":1170,"name":"Fireground","alpha_tag":"Warwick FG","tg_group":"Warwick","frequency":null,"metadata":null,"tags":["Fire-Tac"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"a6f56a71-6f8c-4384-811c-3e356e7c793a","system_id":197,"tgid":1805,"name":"Police","alpha_tag":"W Greenwh PD","tg_group":"West Greenwich","frequency":null,"metadata":null,"tags":["Law Dispatch"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"cf114c62-4dc8-4ace-b8e6-7bff2a60e5b2","system_id":197,"tgid":1806,"name":"Police Secondary","alpha_tag":"W GreenwichPD2","tg_group":"West Greenwich","frequency":null,"metadata":null,"tags":["Law Tac"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"a6c20723-c1b9-4003-a115-b304c0237924","system_id":197,"tgid":1208,"name":"Fire Operations","alpha_tag":"W Warwick FD","tg_group":"West Warwick","frequency":null,"metadata":null,"tags":["Fire Dispatch"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"48794546-a247-4f04-a94d-7a616215e5dd","system_id":197,"tgid":1050,"name":"Police 1 - Dispatch","alpha_tag":"Westerly PD1","tg_group":"Westerly","frequency":null,"metadata":{"encrypted":true},"tags":["Law Dispatch"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"6c40a65b-b6ed-4508-8368-0b14c176c327","system_id":197,"tgid":1051,"name":"Police 2","alpha_tag":"Westerly PD2","tg_group":"Westerly","frequency":null,"metadata":{"encrypted":true},"tags":["Law Talk"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"fdfb1ee2-1962-4000-ab7d-eb4e5de87db2","system_id":197,"tgid":1052,"name":"Police 3","alpha_tag":"Westerly PD3","tg_group":"Westerly","frequency":null,"metadata":{"encrypted":true},"tags":["Law Talk"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"1989d13c-3ab0-462d-9d2a-52ef4ca0d366","system_id":197,"tgid":1053,"name":"Police 4","alpha_tag":"Westerly PD4","tg_group":"Westerly","frequency":null,"metadata":{"encrypted":true},"tags":["Law Talk"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"ae06a314-f50e-4a21-9924-7f814037798c","system_id":197,"tgid":1054,"name":"Police 5 - Reserve Officers","alpha_tag":"Westerly PD5","tg_group":"Westerly","frequency":null,"metadata":null,"tags":["Law Talk"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"c5e10a63-de02-4477-9ecd-eb8a8e0c2792","system_id":197,"tgid":1064,"name":"Police 6 - Traffic Division","alpha_tag":"Westerly PD6","tg_group":"Westerly","frequency":null,"metadata":null,"tags":["Law Talk"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"99272490-106d-4f86-8312-6f60d35772c6","system_id":197,"tgid":1063,"name":"Fire Operations","alpha_tag":"Westerly FD","tg_group":"Westerly","frequency":null,"metadata":null,"tags":["Fire Dispatch"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"dfc744b0-adbf-45dc-b118-c4f2b06cfaf0","system_id":197,"tgid":1072,"name":"Police/Fire/EMS Ops","alpha_tag":"Westerly PFE","tg_group":"Westerly","frequency":null,"metadata":null,"tags":["Multi-Talk"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"77881d73-3a5e-443b-bc46-976647d1c1d3","system_id":197,"tgid":1082,"name":"EMS Operations","alpha_tag":"Westerly EMS ","tg_group":"Westerly","frequency":null,"metadata":null,"tags":["EMS Dispatch"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"f8f6237c-6218-4a86-bb47-080b1f796613","system_id":197,"tgid":1363,"name":"Police 1 - Dispatch","alpha_tag":"Woonskt PD 1","tg_group":"Woonsocket","frequency":null,"metadata":null,"tags":["Law Dispatch"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"7667bd66-6166-4c43-b75b-63390b514bbe","system_id":197,"tgid":1364,"name":"Police 2","alpha_tag":"Woonskt PD 2","tg_group":"Woonsocket","frequency":null,"metadata":{"encrypted":true},"tags":["Law Talk"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"491aa46b-524b-4e1c-9b74-56255fb214c3","system_id":197,"tgid":1360,"name":"Fire Dispatch - Operations","alpha_tag":"Woonsocket FD D","tg_group":"Woonsocket","frequency":null,"metadata":null,"tags":["Fire-Talk"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"f74907b7-ce1c-4a94-a10b-78b5e68f049f","system_id":197,"tgid":1361,"name":"Fire Secondary","alpha_tag":"Woonsocket FD 2","tg_group":"Woonsocket","frequency":null,"metadata":null,"tags":["Fire Dispatch"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"cb002b96-a5d3-4d59-9f6e-977d587abb42","system_id":197,"tgid":1354,"name":"Fire - Fireground 3","alpha_tag":"Woonskt FD 3","tg_group":"Woonsocket","frequency":null,"metadata":null,"tags":["Fire-Tac"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"d0972d5f-3ffc-498b-bcbe-c26f10425576","system_id":197,"tgid":1367,"name":"Citywide","alpha_tag":"Woonskt City","tg_group":"Woonsocket","frequency":null,"metadata":null,"tags":["Multi-Talk"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"1aee1b8a-232d-4035-85dd-276ee1f43c8c","system_id":197,"tgid":1368,"name":"Public Works - Streets","alpha_tag":"Woonsocket PW","tg_group":"Woonsocket","frequency":null,"metadata":null,"tags":["Public Works"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"d7e92a99-3eb1-4107-902f-59ba75f8dd14","system_id":197,"tgid":1,"name":"RISCON Radio Technicians","alpha_tag":"Radio Techs","tg_group":"Radio Technicians","frequency":null,"metadata":null,"tags":["Public Works"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":"42ee3778-6ddb-402d-ab88-dd0ebdbf229f","system_id":197,"tgid":10125,"name":"RISCON Radio Technicians","alpha_tag":"Radio Techs","tg_group":"Radio Technicians","frequency":null,"metadata":null,"tags":["Public Works"],"alert":false,"alert_config":null,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false}] \ No newline at end of file -- 2.47.0