Compare commits
35 commits
fc34e3b1d9
...
328f906000
Author | SHA1 | Date | |
---|---|---|---|
328f906000 | |||
9faecccf2a | |||
36b7773cb0 | |||
1b051c6ad9 | |||
c9a32cd4bf | |||
e82f07e094 | |||
0a88e7f42e | |||
1cb301acdf | |||
641b0c151a | |||
8569ae6d4a | |||
9b93243a4b | |||
af80e46068 | |||
05eccf588b | |||
e38ebe6802 | |||
9ad1ed17c2 | |||
45eb4c9f3e | |||
18866d893c | |||
4f18747255 | |||
fb1b6a475c | |||
f195b6e9b6 | |||
4b2b9399e9 | |||
d592e20d37 | |||
5a82da2b16 | |||
f44ef2ec47 | |||
70a5dd4509 | |||
9e4144545a | |||
657c00e326 | |||
fb3fb4eeab | |||
759c274950 | |||
cecbeb78fe | |||
e97c9ced0e | |||
6e1640e4b4 | |||
f76db949e0 | |||
9046e346b1 | |||
e3a7313806 |
52 changed files with 3336 additions and 755 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -1,6 +1,6 @@
|
|||
config.yaml
|
||||
config.test.yaml
|
||||
mydb.sql
|
||||
/*.sql
|
||||
client/calls/
|
||||
!client/calls/.gitkeep
|
||||
/gordio
|
||||
|
@ -10,3 +10,4 @@ Session.vim
|
|||
*.log
|
||||
*.dlv
|
||||
cover.out
|
||||
backups/
|
||||
|
|
10
.mockery.yaml
Normal file
10
.mockery.yaml
Normal file
|
@ -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:
|
12
Makefile
12
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/
|
||||
|
@ -24,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
|
||||
|
@ -34,5 +36,15 @@ coverage-html:
|
|||
coverage:
|
||||
go test -coverprofile cover.out
|
||||
|
||||
# backup backs up the database without calls
|
||||
backup:
|
||||
sh util/dumpdb.sh
|
||||
|
||||
backupplain:
|
||||
sh util/dumpdb.sh -p
|
||||
|
||||
test:
|
||||
go test -v ./...
|
||||
|
||||
run:
|
||||
go run -v ./cmd/stillbox/ serve
|
||||
|
|
|
@ -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())
|
||||
|
|
1
go.mod
1
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
|
||||
|
|
2
go.sum
2
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=
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package forms
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
@ -10,21 +11,28 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"dynatron.me/x/stillbox/internal/jsontime"
|
||||
"dynatron.me/x/stillbox/internal/jsontypes"
|
||||
|
||||
"github.com/araddon/dateparse"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNotStruct = errors.New("destination is not a struct")
|
||||
ErrNotPointer = errors.New("destination is not a pointer")
|
||||
ErrNotStruct = errors.New("destination is not a struct")
|
||||
ErrNotPointer = errors.New("destination is not a pointer")
|
||||
ErrContentType = errors.New("bad content type")
|
||||
)
|
||||
|
||||
const (
|
||||
MaxMultipartMemory int64 = 1024 * 1024 // 1MB
|
||||
)
|
||||
|
||||
type options struct {
|
||||
tagOverride *string
|
||||
parseTimeIn *time.Location
|
||||
parseLocal bool
|
||||
acceptBlank bool
|
||||
tagOverride *string
|
||||
parseTimeIn *time.Location
|
||||
parseLocal bool
|
||||
acceptBlank bool
|
||||
maxMultipartMemory int64
|
||||
defaultOmitEmpty bool
|
||||
}
|
||||
|
||||
type Option func(*options)
|
||||
|
@ -53,6 +61,18 @@ func WithTag(t string) Option {
|
|||
}
|
||||
}
|
||||
|
||||
func WithMaxMultipartSize(s int64) Option {
|
||||
return func(o *options) {
|
||||
o.maxMultipartMemory = s
|
||||
}
|
||||
}
|
||||
|
||||
func WithOmitEmpty() Option {
|
||||
return func(o *options) {
|
||||
o.defaultOmitEmpty = true
|
||||
}
|
||||
}
|
||||
|
||||
func (o *options) Tag() string {
|
||||
if o.tagOverride != nil {
|
||||
return *o.tagOverride
|
||||
|
@ -147,17 +167,19 @@ func (o *options) parseDuration(s string) (v time.Duration, set bool, err error)
|
|||
return
|
||||
}
|
||||
|
||||
func (o *options) iterFields(r *http.Request, rv reflect.Value) error {
|
||||
rt := rv.Type()
|
||||
for i := 0; i < rv.NumField(); i++ {
|
||||
f := rv.Field(i)
|
||||
tf := rt.Field(i)
|
||||
if !tf.IsExported() && !tf.Anonymous {
|
||||
var typeOfByteSlice = reflect.TypeOf([]byte(nil))
|
||||
|
||||
func (o *options) iterFields(r *http.Request, destStruct reflect.Value) error {
|
||||
structType := destStruct.Type()
|
||||
for i := 0; i < destStruct.NumField(); i++ {
|
||||
destFieldVal := destStruct.Field(i)
|
||||
fieldType := structType.Field(i)
|
||||
if !fieldType.IsExported() && !fieldType.Anonymous {
|
||||
continue
|
||||
}
|
||||
|
||||
if f.Kind() == reflect.Struct && tf.Anonymous {
|
||||
err := o.iterFields(r, f)
|
||||
if destFieldVal.Kind() == reflect.Struct && fieldType.Anonymous {
|
||||
err := o.iterFields(r, destFieldVal)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -165,51 +187,38 @@ func (o *options) iterFields(r *http.Request, rv reflect.Value) error {
|
|||
|
||||
var tAr []string
|
||||
var formField string
|
||||
formTag, has := rt.Field(i).Tag.Lookup(o.Tag())
|
||||
var omitEmpty bool
|
||||
if o.defaultOmitEmpty {
|
||||
omitEmpty = true
|
||||
}
|
||||
|
||||
formTag, has := structType.Field(i).Tag.Lookup(o.Tag())
|
||||
if has {
|
||||
tAr = strings.Split(formTag, ",")
|
||||
formField = tAr[0]
|
||||
for _, v := range tAr[1:] {
|
||||
if v == "omitempty" {
|
||||
omitEmpty = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !has || formField == "-" {
|
||||
continue
|
||||
}
|
||||
|
||||
fi := f.Interface()
|
||||
destFieldIntf := destFieldVal.Interface()
|
||||
|
||||
switch v := fi.(type) {
|
||||
case string, *string:
|
||||
s := r.Form.Get(formField)
|
||||
setVal(f, s != "" || o.acceptBlank, v, s)
|
||||
case int, uint, *int, *uint:
|
||||
ff := r.Form.Get(formField)
|
||||
val, set, err := o.parseInt(ff)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
setVal(f, set, v, val)
|
||||
case float64:
|
||||
ff := r.Form.Get(formField)
|
||||
val, set, err := o.parseFloat64(ff)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
setVal(f, set, v, val)
|
||||
case bool, *bool:
|
||||
ff := r.Form.Get(formField)
|
||||
val, set, err := o.parseBool(ff)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
setVal(f, set, v, val)
|
||||
case []byte:
|
||||
if destFieldVal.Kind() == reflect.Slice && destFieldVal.Type() == typeOfByteSlice {
|
||||
file, hdr, err := r.FormFile(formField)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get form file: %w", err)
|
||||
}
|
||||
|
||||
nameField, hasFilename := rt.Field(i).Tag.Lookup("filenameField")
|
||||
nameField, hasFilename := structType.Field(i).Tag.Lookup("filenameField")
|
||||
if hasFilename {
|
||||
fnf := rv.FieldByName(nameField)
|
||||
fnf := destStruct.FieldByName(nameField)
|
||||
if fnf == (reflect.Value{}) {
|
||||
panic(fmt.Errorf("filenameField '%s' does not exist", nameField))
|
||||
}
|
||||
|
@ -221,23 +230,52 @@ func (o *options) iterFields(r *http.Request, rv reflect.Value) error {
|
|||
return fmt.Errorf("file read: %w", err)
|
||||
}
|
||||
|
||||
f.SetBytes(audioBytes)
|
||||
case time.Time, *time.Time, jsontime.Time, *jsontime.Time:
|
||||
tval := r.Form.Get(formField)
|
||||
t, set, err := o.parseTime(tval)
|
||||
destFieldVal.SetBytes(audioBytes)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if !r.Form.Has(formField) && omitEmpty {
|
||||
continue
|
||||
}
|
||||
|
||||
ff := r.Form.Get(formField)
|
||||
|
||||
switch v := destFieldIntf.(type) {
|
||||
case string, *string:
|
||||
setVal(destFieldVal, ff != "" || o.acceptBlank, ff)
|
||||
case int, uint, *int, *uint:
|
||||
val, set, err := o.parseInt(ff)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
setVal(f, set, v, t)
|
||||
case time.Duration, *time.Duration, jsontime.Duration, *jsontime.Duration:
|
||||
dval := r.Form.Get(formField)
|
||||
d, set, err := o.parseDuration(dval)
|
||||
setVal(destFieldVal, set, val)
|
||||
case float64:
|
||||
val, set, err := o.parseFloat64(ff)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
setVal(f, set, v, d)
|
||||
setVal(destFieldVal, set, val)
|
||||
case bool, *bool:
|
||||
val, set, err := o.parseBool(ff)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
setVal(destFieldVal, set, val)
|
||||
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, jsontypes.Duration, *jsontypes.Duration:
|
||||
d, set, err := o.parseDuration(ff)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
setVal(destFieldVal, set, d)
|
||||
case []int:
|
||||
val := strings.Trim(r.Form.Get(formField), "[]")
|
||||
val := strings.Trim(ff, "[]")
|
||||
if val == "" && o.acceptBlank {
|
||||
continue
|
||||
}
|
||||
|
@ -249,7 +287,7 @@ func (o *options) iterFields(r *http.Request, rv reflect.Value) error {
|
|||
ar = append(ar, i)
|
||||
}
|
||||
}
|
||||
f.Set(reflect.ValueOf(ar))
|
||||
destFieldVal.Set(reflect.ValueOf(ar))
|
||||
default:
|
||||
panic(fmt.Errorf("unsupported type %T", v))
|
||||
}
|
||||
|
@ -258,48 +296,77 @@ func (o *options) iterFields(r *http.Request, rv reflect.Value) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func setVal(setField reflect.Value, set bool, fv any, sv any) {
|
||||
func setVal(destFieldVal reflect.Value, set bool, src any) {
|
||||
if !set {
|
||||
return
|
||||
}
|
||||
|
||||
rv := reflect.TypeOf(fv)
|
||||
svo := reflect.ValueOf(sv)
|
||||
destType := destFieldVal.Type()
|
||||
srcVal := reflect.ValueOf(src)
|
||||
|
||||
if svo.CanConvert(rv) {
|
||||
svo = svo.Convert(rv)
|
||||
if srcVal.Kind() == reflect.Ptr {
|
||||
srcVal = srcVal.Elem()
|
||||
}
|
||||
|
||||
if rv.Kind() == reflect.Ptr {
|
||||
svo = svo.Addr()
|
||||
if destType.Kind() == reflect.Ptr {
|
||||
if !srcVal.CanAddr() {
|
||||
if srcVal.CanConvert(destType.Elem()) {
|
||||
srcVal = srcVal.Convert(destType.Elem())
|
||||
}
|
||||
copy := reflect.New(srcVal.Type())
|
||||
copy.Elem().Set(srcVal)
|
||||
srcVal = copy
|
||||
}
|
||||
} else if srcVal.CanConvert(destFieldVal.Type()) {
|
||||
srcVal = srcVal.Convert(destFieldVal.Type())
|
||||
}
|
||||
|
||||
setField.Set(svo)
|
||||
destFieldVal.Set(srcVal)
|
||||
}
|
||||
|
||||
func Unmarshal(r *http.Request, dest any, opt ...Option) error {
|
||||
o := options{}
|
||||
o := options{
|
||||
maxMultipartMemory: MaxMultipartMemory,
|
||||
}
|
||||
|
||||
for _, opt := range opt {
|
||||
opt(&o)
|
||||
}
|
||||
|
||||
rv := reflect.ValueOf(dest)
|
||||
if k := rv.Kind(); k == reflect.Ptr {
|
||||
rv = rv.Elem()
|
||||
} else {
|
||||
return ErrNotPointer
|
||||
}
|
||||
contentType := strings.Split(r.Header.Get("Content-Type"), ";")[0]
|
||||
|
||||
if rv.Kind() != reflect.Struct {
|
||||
return ErrNotStruct
|
||||
}
|
||||
switch contentType {
|
||||
case "multipart/form-data":
|
||||
err := r.ParseMultipartForm(o.maxMultipartMemory)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ParseForm: %w", err)
|
||||
}
|
||||
|
||||
if strings.HasPrefix(r.Header.Get("Content-Type"), "application/x-www-form-urlencoded") {
|
||||
return o.unmarshalForm(r, dest)
|
||||
case "application/x-www-form-urlencoded":
|
||||
err := r.ParseForm()
|
||||
if err != nil {
|
||||
return fmt.Errorf("ParseForm: %w", err)
|
||||
}
|
||||
return o.unmarshalForm(r, dest)
|
||||
case "application/json":
|
||||
return json.NewDecoder(r.Body).Decode(dest)
|
||||
}
|
||||
|
||||
return o.iterFields(r, rv)
|
||||
return ErrContentType
|
||||
}
|
||||
|
||||
func (o *options) unmarshalForm(r *http.Request, dest any) error {
|
||||
destVal := reflect.ValueOf(dest)
|
||||
if k := destVal.Kind(); k == reflect.Ptr {
|
||||
destVal = destVal.Elem()
|
||||
} else {
|
||||
return ErrNotPointer
|
||||
}
|
||||
|
||||
if destVal.Kind() != reflect.Struct {
|
||||
return ErrNotStruct
|
||||
}
|
||||
|
||||
return o.iterFields(r, destVal)
|
||||
}
|
||||
|
|
|
@ -8,8 +8,9 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"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"
|
||||
|
@ -47,11 +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 *jsontypes.Duration `form:"halfLife"`
|
||||
Recent *string `form:"recent"`
|
||||
ScoreStart *jsontypes.Time `form:"scoreStart"`
|
||||
ScoreEnd jsontypes.Time `form:"scoreEnd"`
|
||||
}
|
||||
|
||||
var (
|
||||
|
@ -64,26 +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(jsontypes.Duration(30 * time.Minute)),
|
||||
Recent: common.PtrTo("2h0m0s"),
|
||||
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{
|
||||
|
@ -122,29 +138,29 @@ func TestUnmarshal(t *testing.T) {
|
|||
name string
|
||||
r *http.Request
|
||||
dest any
|
||||
compare any
|
||||
expect any
|
||||
expectErr error
|
||||
opts []forms.Option
|
||||
}{
|
||||
{
|
||||
name: "base case",
|
||||
r: makeRequest("call1.http"),
|
||||
dest: &callUploadRequest{},
|
||||
compare: &Call1,
|
||||
opts: []forms.Option{forms.WithAcceptBlank()},
|
||||
name: "base case",
|
||||
r: makeRequest("call1.http"),
|
||||
dest: &callUploadRequest{},
|
||||
expect: &Call1,
|
||||
opts: []forms.Option{forms.WithAcceptBlank()},
|
||||
},
|
||||
{
|
||||
name: "base case no accept blank",
|
||||
r: makeRequest("call1.http"),
|
||||
dest: &callUploadRequest{},
|
||||
compare: &Call1,
|
||||
expect: &Call1,
|
||||
expectErr: errors.New(`parsebool(''): strconv.ParseBool: parsing "": invalid syntax`),
|
||||
},
|
||||
{
|
||||
name: "not a pointer",
|
||||
r: makeRequest("call1.http"),
|
||||
dest: callUploadRequest{},
|
||||
compare: callUploadRequest{},
|
||||
expect: callUploadRequest{},
|
||||
expectErr: forms.ErrNotPointer,
|
||||
opts: []forms.Option{forms.WithAcceptBlank()},
|
||||
},
|
||||
|
@ -152,7 +168,7 @@ func TestUnmarshal(t *testing.T) {
|
|||
name: "not a struct",
|
||||
r: makeRequest("call1.http"),
|
||||
dest: &str,
|
||||
compare: callUploadRequest{},
|
||||
expect: callUploadRequest{},
|
||||
expectErr: forms.ErrNotStruct,
|
||||
opts: []forms.Option{forms.WithAcceptBlank()},
|
||||
},
|
||||
|
@ -160,44 +176,51 @@ func TestUnmarshal(t *testing.T) {
|
|||
name: "url encoded",
|
||||
r: makeRequest("urlenc.http"),
|
||||
dest: &urlEncTest{},
|
||||
compare: &UrlEncTest,
|
||||
expect: &UrlEncTest,
|
||||
expectErr: errors.New(`Could not find format for ""`),
|
||||
},
|
||||
{
|
||||
name: "url encoded accept blank",
|
||||
r: makeRequest("urlenc.http"),
|
||||
dest: &urlEncTest{},
|
||||
compare: &UrlEncTest,
|
||||
opts: []forms.Option{forms.WithAcceptBlank()},
|
||||
name: "url encoded accept blank",
|
||||
r: makeRequest("urlenc.http"),
|
||||
dest: &urlEncTest{},
|
||||
expect: &UrlEncTest,
|
||||
opts: []forms.Option{forms.WithAcceptBlank()},
|
||||
},
|
||||
{
|
||||
name: "url encoded accept blank pointer",
|
||||
r: makeRequest("urlenc.http"),
|
||||
dest: &ptrTestJT{},
|
||||
expect: &PtrTestJT,
|
||||
opts: []forms.Option{forms.WithAcceptBlank()},
|
||||
},
|
||||
{
|
||||
name: "url encoded jsontime",
|
||||
r: makeRequest("urlenc.http"),
|
||||
dest: &urlEncTestJT{},
|
||||
compare: &UrlEncTestJT,
|
||||
expect: &UrlEncTestJT,
|
||||
expectErr: errors.New(`Could not find format for ""`),
|
||||
opts: []forms.Option{forms.WithTag("json")},
|
||||
},
|
||||
{
|
||||
name: "url encoded jsontime with tz",
|
||||
r: makeRequest("urlenc.http"),
|
||||
dest: &urlEncTestJT{},
|
||||
compare: &UrlEncTestJT,
|
||||
opts: []forms.Option{forms.WithAcceptBlank(), forms.WithParseTimeInTZ(time.UTC), forms.WithTag("json")},
|
||||
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 jsontime with local",
|
||||
r: makeRequest("urlenc.http"),
|
||||
dest: &urlEncTestJT{},
|
||||
compare: &UrlEncTestJTLocal,
|
||||
opts: []forms.Option{forms.WithAcceptBlank(), forms.WithParseLocalTime(), forms.WithTag("json")},
|
||||
name: "url encoded jsontime with local",
|
||||
r: makeRequest("urlenc.http"),
|
||||
dest: &urlEncTestJT{},
|
||||
expect: &UrlEncTestJTLocal,
|
||||
opts: []forms.Option{forms.WithAcceptBlank(), forms.WithParseLocalTime(), forms.WithTag("json")},
|
||||
},
|
||||
{
|
||||
name: "sim real data",
|
||||
r: makeRequest("urlenc2.http"),
|
||||
dest: &alerting.Simulation{},
|
||||
compare: realSim,
|
||||
opts: []forms.Option{forms.WithAcceptBlank(), forms.WithParseLocalTime()},
|
||||
name: "sim real data",
|
||||
r: makeRequest("urlenc2.http"),
|
||||
dest: &alerting.Simulation{},
|
||||
expect: realSim,
|
||||
opts: []forms.Option{forms.WithAcceptBlank(), forms.WithParseLocalTime()},
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -209,7 +232,7 @@ func TestUnmarshal(t *testing.T) {
|
|||
assert.Contains(t, tc.expectErr.Error(), err.Error())
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tc.compare, tc.dest)
|
||||
assert.Equal(t, tc.expect, tc.dest)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package jsontime
|
||||
package jsontypes
|
||||
|
||||
import (
|
||||
"encoding/json"
|
3
internal/jsontypes/metadata.go
Normal file
3
internal/jsontypes/metadata.go
Normal file
|
@ -0,0 +1,3 @@
|
|||
package jsontypes
|
||||
|
||||
type Metadata map[string]interface{}
|
|
@ -36,7 +36,8 @@ func (a *Alert) ToAddAlertParams() database.AddAlertParams {
|
|||
return database.AddAlertParams{
|
||||
ID: a.ID,
|
||||
Time: pgtype.Timestamptz{Time: a.Timestamp, Valid: true},
|
||||
PackedTg: a.Score.ID.Pack(),
|
||||
SystemID: int(a.Score.ID.System),
|
||||
TGID: int(a.Score.ID.Talkgroup),
|
||||
Weight: &a.Weight,
|
||||
Score: &f32score,
|
||||
OrigScore: origScore,
|
||||
|
@ -44,8 +45,7 @@ func (a *Alert) ToAddAlertParams() database.AddAlertParams {
|
|||
}
|
||||
}
|
||||
|
||||
// makeAlert creates a notification for later rendering by the template.
|
||||
// It takes a talkgroup Score as input.
|
||||
// Make creates an alert for later rendering or storage.
|
||||
func Make(ctx context.Context, store talkgroups.Store, score trending.Score[talkgroups.ID], origScore float64) (Alert, error) {
|
||||
d := Alert{
|
||||
ID: uuid.New(),
|
||||
|
|
|
@ -228,11 +228,11 @@ func (as *alerter) scoredTGs() []talkgroups.ID {
|
|||
return tgs
|
||||
}
|
||||
|
||||
// packedScoredTGs gets a list of packed TGIDs.
|
||||
func (as *alerter) packedScoredTGs() []int64 {
|
||||
tgs := make([]int64, 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.Pack())
|
||||
tgs.Append(s.ID.System, s.ID.Talkgroup)
|
||||
}
|
||||
|
||||
return tgs
|
||||
|
@ -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
|
||||
}
|
||||
|
|
40
pkg/alerting/rules/alertrules.go
Normal file
40
pkg/alerting/rules/alertrules.go
Normal file
|
@ -0,0 +1,40 @@
|
|||
package rules
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"dynatron.me/x/stillbox/internal/ruletime"
|
||||
)
|
||||
|
||||
type AlertRules []AlertRule
|
||||
|
||||
func (ars AlertRules) Apply(t time.Time, coversOpts ...ruletime.CoversOption) float64 {
|
||||
if ars == nil || len(ars) < 1 {
|
||||
return 1.0
|
||||
}
|
||||
|
||||
final := 1.0
|
||||
|
||||
for _, ar := range ars {
|
||||
if ar.MatchTime(t, coversOpts...) {
|
||||
final *= float64(ar.ScoreMultiplier)
|
||||
}
|
||||
}
|
||||
|
||||
return final
|
||||
}
|
||||
|
||||
type AlertRule struct {
|
||||
Times []ruletime.RuleTime `json:"times"`
|
||||
ScoreMultiplier float32 `json:"mult"`
|
||||
}
|
||||
|
||||
func (ar *AlertRule) MatchTime(t time.Time, coversOpts ...ruletime.CoversOption) bool {
|
||||
for _, at := range ar.Times {
|
||||
if at.Covers(t, coversOpts...) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
package talkgroups_test
|
||||
package rules_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"math"
|
||||
"testing"
|
||||
|
@ -8,6 +9,7 @@ import (
|
|||
|
||||
"dynatron.me/x/stillbox/internal/ruletime"
|
||||
"dynatron.me/x/stillbox/internal/trending"
|
||||
"dynatron.me/x/stillbox/pkg/alerting/rules"
|
||||
"dynatron.me/x/stillbox/pkg/talkgroups"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
@ -15,19 +17,18 @@ import (
|
|||
)
|
||||
|
||||
func TestAlertConfig(t *testing.T) {
|
||||
ac := talkgroups.NewAlertConfig()
|
||||
parseTests := []struct {
|
||||
name string
|
||||
tg talkgroups.ID
|
||||
conf string
|
||||
compare []talkgroups.AlertRule
|
||||
compare rules.AlertRules
|
||||
expectErr error
|
||||
}{
|
||||
{
|
||||
name: "base case",
|
||||
tg: talkgroups.TG(197, 3),
|
||||
conf: `[{"times":["7:00+2h","01:00+1h","16:00+1h","19:00+4h"],"mult":0.2},{"times":["11:00+1h","15:00+30m","16:03+20m"],"mult":2.0}]`,
|
||||
compare: []talkgroups.AlertRule{
|
||||
compare: rules.AlertRules{
|
||||
{
|
||||
Times: []ruletime.RuleTime{
|
||||
ruletime.Must(ruletime.New("7:00+2h")),
|
||||
|
@ -55,14 +56,18 @@ func TestAlertConfig(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
tgc := make(map[talkgroups.ID]rules.AlertRules)
|
||||
|
||||
for _, tc := range parseTests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
err := ac.UnmarshalTGRules(tc.tg, []byte(tc.conf))
|
||||
var ar rules.AlertRules
|
||||
err := json.Unmarshal([]byte(tc.conf), &ar)
|
||||
if tc.expectErr != nil {
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), tc.expectErr.Error())
|
||||
} else {
|
||||
assert.Equal(t, tc.compare, ac.GetRules(tc.tg))
|
||||
tgc[tc.tg] = ar
|
||||
assert.Equal(t, tc.compare, ar)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -126,7 +131,7 @@ func TestAlertConfig(t *testing.T) {
|
|||
ID: tc.tg,
|
||||
Score: tc.origScore,
|
||||
}
|
||||
assert.Equal(t, tc.expectScore, toFixed(cs.Score*ac.ApplyAlertRules(cs.ID, tc.t), 5))
|
||||
assert.Equal(t, tc.expectScore, toFixed(cs.Score*tgc[cs.ID].Apply(tc.t), 5))
|
||||
})
|
||||
}
|
||||
}
|
|
@ -2,7 +2,6 @@ package alerting
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
@ -10,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"
|
||||
|
@ -24,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:"-"`
|
||||
|
@ -65,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()).
|
||||
|
@ -114,24 +113,15 @@ func (s *Simulation) Simulate(ctx context.Context) (trending.Scores[talkgroups.I
|
|||
func (as *alerter) simulateHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
s := new(Simulation)
|
||||
switch r.Header.Get("Content-Type") {
|
||||
case "application/json":
|
||||
err := json.NewDecoder(r.Body).Decode(s)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("simulate decode: %w", err)
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
default:
|
||||
err := forms.Unmarshal(r, s, forms.WithAcceptBlank(), forms.WithParseLocalTime())
|
||||
if err != nil {
|
||||
err = fmt.Errorf("simulate unmarshal: %w", err)
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
err := forms.Unmarshal(r, s, forms.WithAcceptBlank(), forms.WithParseLocalTime())
|
||||
if err != nil {
|
||||
err = fmt.Errorf("simulate unmarshal: %w", err)
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
err := s.verify()
|
||||
err = s.verify()
|
||||
if err != nil {
|
||||
err = fmt.Errorf("simulation profile verify: %w", err)
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
|
|
|
@ -40,20 +40,20 @@ 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.GetTalkgroupsWithLearnedBySysTGID(ctx, as.scoredTGsTuple())
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("stats TG get failed")
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
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
|
||||
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
|
||||
|
|
127
pkg/api/api.go
127
pkg/api/api.go
|
@ -1,127 +0,0 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"dynatron.me/x/stillbox/pkg/talkgroups"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-viper/mapstructure/v2"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type API interface {
|
||||
Subrouter() http.Handler
|
||||
}
|
||||
|
||||
type api struct {
|
||||
tgs talkgroups.Store
|
||||
}
|
||||
|
||||
func New(tgs talkgroups.Store) API {
|
||||
s := &api{
|
||||
tgs: tgs,
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (a *api) Subrouter() http.Handler {
|
||||
r := chi.NewMux()
|
||||
|
||||
r.Get("/talkgroup/{system:\\d+}/{id:\\d+}", a.talkgroup)
|
||||
r.Get("/talkgroup/{system:\\d+}/", a.talkgroup)
|
||||
r.Get("/talkgroup/", a.talkgroup)
|
||||
return r
|
||||
}
|
||||
|
||||
var statusMapping = map[error]int{
|
||||
talkgroups.ErrNotFound: http.StatusNotFound,
|
||||
pgx.ErrNoRows: http.StatusNotFound,
|
||||
}
|
||||
|
||||
func httpCode(err error) int {
|
||||
c, ok := statusMapping[err]
|
||||
if ok {
|
||||
return c
|
||||
}
|
||||
|
||||
for e, c := range statusMapping { // check if err wraps an error we know about
|
||||
if errors.Is(err, e) {
|
||||
return c
|
||||
}
|
||||
}
|
||||
|
||||
return http.StatusInternalServerError
|
||||
}
|
||||
|
||||
func (a *api) writeResponse(w http.ResponseWriter, r *http.Request, data interface{}, err error) {
|
||||
if err != nil {
|
||||
log.Error().Str("path", r.URL.Path).Err(err).Msg("request failed")
|
||||
http.Error(w, err.Error(), httpCode(err))
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
enc := json.NewEncoder(w)
|
||||
err = enc.Encode(data)
|
||||
if err != nil {
|
||||
log.Error().Str("path", r.URL.Path).Err(err).Msg("response marshal failed")
|
||||
http.Error(w, err.Error(), httpCode(err))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func decodeParams(d interface{}, r *http.Request) error {
|
||||
params := chi.RouteContext(r.Context()).URLParams
|
||||
m := make(map[string]string, len(params.Keys))
|
||||
|
||||
for i, k := range params.Keys {
|
||||
m[k] = params.Values[i]
|
||||
}
|
||||
|
||||
dec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
|
||||
Metadata: nil,
|
||||
Result: d,
|
||||
TagName: "param",
|
||||
WeaklyTypedInput: true,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return dec.Decode(m)
|
||||
}
|
||||
|
||||
func (a *api) badReq(w http.ResponseWriter, err error) {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
func (a *api) talkgroup(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
p := struct {
|
||||
System *int `param:"system"`
|
||||
ID *int `param:"id"`
|
||||
}{}
|
||||
|
||||
err := decodeParams(&p, r)
|
||||
if err != nil {
|
||||
a.badReq(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
var res interface{}
|
||||
switch {
|
||||
case p.System != nil && p.ID != nil:
|
||||
res, err = a.tgs.TG(ctx, talkgroups.TG(*p.System, *p.ID))
|
||||
case p.System != nil:
|
||||
res, err = a.tgs.SystemTGs(ctx, int32(*p.System))
|
||||
default:
|
||||
res, err = a.tgs.TGs(ctx, nil)
|
||||
}
|
||||
|
||||
a.writeResponse(w, r, res, err)
|
||||
}
|
|
@ -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")
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
</head>
|
||||
<body>
|
||||
<div>
|
||||
<form action="/login" method="POST">
|
||||
<form action="/api/login" method="POST">
|
||||
<label for="username">Username: </label>
|
||||
<input type="text" name="username" />
|
||||
<label for="password">Password: </label>
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
@ -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 {
|
||||
|
@ -54,12 +55,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
|
||||
|
|
|
@ -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
|
||||
|
@ -13,7 +13,7 @@ import (
|
|||
)
|
||||
|
||||
const addAlert = `-- name: AddAlert :exec
|
||||
INSERT INTO alerts (id, time, talkgroup, weight, score, orig_score, notified, metadata)
|
||||
INSERT INTO alerts (id, time, tgid, system_id, weight, score, orig_score, notified, metadata)
|
||||
VALUES
|
||||
(
|
||||
$1,
|
||||
|
@ -23,14 +23,16 @@ VALUES
|
|||
$5,
|
||||
$6,
|
||||
$7,
|
||||
$8
|
||||
$8,
|
||||
$9
|
||||
)
|
||||
`
|
||||
|
||||
type AddAlertParams struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
Time pgtype.Timestamptz `json:"time"`
|
||||
PackedTg int64 `json:"packed_tg"`
|
||||
TGID int `json:"tgid"`
|
||||
SystemID int `json:"system_id"`
|
||||
Weight *float32 `json:"weight"`
|
||||
Score *float32 `json:"score"`
|
||||
OrigScore *float32 `json:"orig_score"`
|
||||
|
@ -42,7 +44,8 @@ func (q *Queries) AddAlert(ctx context.Context, arg AddAlertParams) error {
|
|||
_, err := q.db.Exec(ctx, addAlert,
|
||||
arg.ID,
|
||||
arg.Time,
|
||||
arg.PackedTg,
|
||||
arg.TGID,
|
||||
arg.SystemID,
|
||||
arg.Weight,
|
||||
arg.Score,
|
||||
arg.OrigScore,
|
||||
|
|
|
@ -10,17 +10,39 @@ import (
|
|||
"github.com/golang-migrate/migrate/v4"
|
||||
_ "github.com/golang-migrate/migrate/v4/database/pgx/v5"
|
||||
"github.com/golang-migrate/migrate/v4/source/iofs"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
"github.com/jackc/pgx/v5/tracelog"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// 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) {
|
||||
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) {
|
||||
func NewClient(ctx context.Context, conf config.DB) (DB, error) {
|
||||
dir, err := iofs.New(sqlembed.Migrations, "postgres/migrations")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -43,12 +65,19 @@ func NewClient(ctx context.Context, conf config.DB) (*DB, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
if conf.LogQueries {
|
||||
pgConf.ConnConfig.Tracer = &tracelog.TraceLog{
|
||||
Logger: dbLogger{},
|
||||
LogLevel: tracelog.LogLevelTrace,
|
||||
}
|
||||
}
|
||||
|
||||
pool, err := pgxpool.NewWithConfig(ctx, pgConf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
db := &DB{
|
||||
db := &Database{
|
||||
Pool: pool,
|
||||
Queries: New(pool),
|
||||
}
|
||||
|
@ -56,13 +85,13 @@ func NewClient(ctx context.Context, conf config.DB) (*DB, error) {
|
|||
return db, nil
|
||||
}
|
||||
|
||||
type DBCtxKey string
|
||||
type dBCtxKey string
|
||||
|
||||
const DBCTXKeyValue DBCtxKey = "dbctx"
|
||||
const DBCtxKey dBCtxKey = "dbctx"
|
||||
|
||||
// FromCtx returns the database handle from the provided Context.
|
||||
func FromCtx(ctx context.Context) *DB {
|
||||
c, ok := ctx.Value(DBCTXKeyValue).(*DB)
|
||||
func FromCtx(ctx context.Context) DB {
|
||||
c, ok := ctx.Value(DBCtxKey).(DB)
|
||||
if !ok {
|
||||
panic("no DB in context")
|
||||
}
|
||||
|
@ -71,12 +100,12 @@ func FromCtx(ctx context.Context) *DB {
|
|||
}
|
||||
|
||||
// CtxWithDB returns a Context with the provided database handle.
|
||||
func CtxWithDB(ctx context.Context, conn *DB) context.Context {
|
||||
return context.WithValue(ctx, DBCTXKeyValue, conn)
|
||||
func CtxWithDB(ctx context.Context, conn DB) context.Context {
|
||||
return context.WithValue(ctx, DBCtxKey, conn)
|
||||
}
|
||||
|
||||
// IsNoRows is a convenience function that returns whether a returned error is a database
|
||||
// no rows error.
|
||||
func IsNoRows(err error) bool {
|
||||
return strings.Contains(err.Error(), "no rows in result set")
|
||||
return errors.Is(err, pgx.ErrNoRows)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.26.0
|
||||
// sqlc v1.27.0
|
||||
|
||||
package database
|
||||
|
||||
|
|
|
@ -1,11 +1,17 @@
|
|||
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 (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 (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 }
|
||||
|
|
1631
pkg/database/mocks/DB.go
Normal file
1631
pkg/database/mocks/DB.go
Normal file
File diff suppressed because it is too large
Load diff
|
@ -1,12 +1,14 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.26.0
|
||||
// sqlc v1.27.0
|
||||
|
||||
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"
|
||||
)
|
||||
|
@ -14,9 +16,8 @@ import (
|
|||
type Alert struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
Time pgtype.Timestamptz `json:"time"`
|
||||
Talkgroup int64 `json:"talkgroup"`
|
||||
SystemID int32 `json:"system_id"`
|
||||
Tgid int32 `json:"tgid"`
|
||||
TGID int `json:"tgid"`
|
||||
SystemID int `json:"system_id"`
|
||||
Weight *float32 `json:"weight"`
|
||||
Score *float32 `json:"score"`
|
||||
OrigScore *float32 `json:"orig_score"`
|
||||
|
@ -82,24 +83,24 @@ 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 []byte `json:"alert_config"`
|
||||
Weight float32 `json:"weight"`
|
||||
ID uuid.UUID `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 {
|
||||
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"`
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.26.0
|
||||
// sqlc v1.27.0
|
||||
|
||||
package database
|
||||
|
||||
|
@ -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 int64, 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,23 +21,22 @@ 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)
|
||||
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)
|
||||
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, 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)
|
||||
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)
|
||||
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)
|
||||
}
|
||||
|
||||
var _ Querier = (*Queries)(nil)
|
||||
|
|
132
pkg/database/talkgroups.go
Normal file
132
pkg/database/talkgroups.go
Normal file
|
@ -0,0 +1,132 @@
|
|||
package database
|
||||
|
||||
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 {
|
||||
return [2][]uint32{
|
||||
make([]uint32, 0, cap),
|
||||
make([]uint32, 0, cap),
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TGTuples) Append(sys, tg uint32) {
|
||||
t[0] = append(t[0], sys)
|
||||
t[1] = append(t[1], tg)
|
||||
}
|
||||
|
||||
// Below queries are here because sqlc refuses to parse unnest(x, y)
|
||||
|
||||
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
|
||||
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)
|
||||
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
|
||||
JOIN UNNEST($1::INT4[], $2::INT4[]) AS tgt(sys, tg) ON (tgl.system_id = tgt.sys AND tgl.tgid = tgt.tg);`
|
||||
|
||||
type GetTalkgroupsRow struct {
|
||||
Talkgroup Talkgroup `json:"talkgroup"`
|
||||
System System `json:"system"`
|
||||
Learned bool `json:"learned"`
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetTalkgroupsRow
|
||||
for rows.Next() {
|
||||
var i GetTalkgroupsRow
|
||||
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 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);`
|
||||
|
||||
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
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetTalkgroupsRow
|
||||
for rows.Next() {
|
||||
var i GetTalkgroupsRow
|
||||
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 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
|
||||
}
|
|
@ -1,24 +1,18 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.26.0
|
||||
// sqlc v1.27.0
|
||||
// source: talkgroups.sql
|
||||
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"dynatron.me/x/stillbox/internal/jsontypes"
|
||||
"dynatron.me/x/stillbox/pkg/alerting/rules"
|
||||
"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 int64, 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
|
||||
`
|
||||
|
@ -32,20 +26,20 @@ 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) {
|
||||
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,
|
||||
&i.Talkgroup.SystemID,
|
||||
&i.Talkgroup.Tgid,
|
||||
&i.Talkgroup.TGID,
|
||||
&i.Talkgroup.Name,
|
||||
&i.Talkgroup.AlphaTag,
|
||||
&i.Talkgroup.TgGroup,
|
||||
|
@ -68,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
|
||||
}
|
||||
|
@ -80,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)
|
||||
|
@ -93,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
|
||||
|
@ -109,10 +103,10 @@ 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,
|
||||
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,
|
||||
|
@ -128,13 +122,13 @@ type GetTalkgroupWithLearnedRow struct {
|
|||
Learned bool `json:"learned"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetTalkgroupWithLearned(ctx context.Context, systemID int, tgid int) (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,
|
||||
|
@ -151,52 +145,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]
|
||||
|
@ -218,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,
|
||||
|
@ -260,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,
|
||||
|
@ -289,7 +237,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,
|
||||
|
@ -317,68 +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.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 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.TGID,
|
||||
&i.Talkgroup.Name,
|
||||
&i.Talkgroup.AlphaTag,
|
||||
&i.Talkgroup.TgGroup,
|
||||
|
@ -411,7 +298,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,
|
||||
|
@ -439,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,
|
||||
|
@ -464,11 +351,75 @@ 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
|
||||
}
|
||||
|
||||
const updateTalkgroup = `-- name: UpdateTalkgroup :one
|
||||
UPDATE talkgroups
|
||||
SET
|
||||
name = COALESCE($1, name),
|
||||
alpha_tag = COALESCE($2, alpha_tag),
|
||||
tg_group = COALESCE($3, tg_group),
|
||||
frequency = COALESCE($4, frequency),
|
||||
metadata = COALESCE($5, metadata),
|
||||
tags = COALESCE($6, tags),
|
||||
alert = COALESCE($7, alert),
|
||||
alert_config = COALESCE($8, alert_config),
|
||||
weight = COALESCE($9, weight)
|
||||
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
|
||||
`
|
||||
|
||||
type UpdateTalkgroupParams struct {
|
||||
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 pgtype.UUID `json:"id"`
|
||||
SystemID *int32 `json:"system_id"`
|
||||
TGID *int32 `json:"tgid"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateTalkgroup(ctx context.Context, arg UpdateTalkgroupParams) (Talkgroup, error) {
|
||||
row := q.db.QueryRow(ctx, updateTalkgroup,
|
||||
arg.Name,
|
||||
arg.AlphaTag,
|
||||
arg.TgGroup,
|
||||
arg.Frequency,
|
||||
arg.Metadata,
|
||||
arg.Tags,
|
||||
arg.Alert,
|
||||
arg.AlertConfig,
|
||||
arg.Weight,
|
||||
arg.ID,
|
||||
arg.SystemID,
|
||||
arg.TGID,
|
||||
)
|
||||
var i Talkgroup
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.SystemID,
|
||||
&i.TGID,
|
||||
&i.Name,
|
||||
&i.AlphaTag,
|
||||
&i.TgGroup,
|
||||
&i.Frequency,
|
||||
&i.Metadata,
|
||||
&i.Tags,
|
||||
&i.Alert,
|
||||
&i.AlertConfig,
|
||||
&i.Weight,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
|
|
@ -6,34 +6,16 @@ import (
|
|||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const getTalkgroupWithLearnedByPackedIDsTest = `-- name: GetTalkgroupWithLearnedByPackedIDs :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,14 +34,14 @@ 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,
|
||||
TRUE learned
|
||||
FROM talkgroups_learned tgl
|
||||
JOIN systems sys ON tgl.system_id = sys.id
|
||||
WHERE tg.system_id = $1 AND ignored IS NOT TRUE
|
||||
WHERE tgl.system_id = $1 AND ignored IS NOT TRUE
|
||||
`
|
||||
|
||||
const getTalkgroupsWithLearnedTest = `-- name: GetTalkgroupsWithLearned :many
|
||||
|
@ -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, getTalkgroupWithLearnedByPackedIDs)
|
||||
require.Equal(t, getTalkgroupWithLearnedTest, getTalkgroupWithLearned)
|
||||
require.Equal(t, getTalkgroupsWithLearnedBySystemTest, getTalkgroupsWithLearnedBySystem)
|
||||
require.Equal(t, getTalkgroupsWithLearnedTest, getTalkgroupsWithLearned)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
132
pkg/rest/api.go
Normal file
132
pkg/rest/api.go
Normal file
|
@ -0,0 +1,132 @@
|
|||
package rest
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"dynatron.me/x/stillbox/pkg/talkgroups"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/render"
|
||||
"github.com/go-viper/mapstructure/v2"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type API interface {
|
||||
Subrouter() http.Handler
|
||||
}
|
||||
|
||||
type api struct {
|
||||
}
|
||||
|
||||
func New() API {
|
||||
s := new(api)
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (a *api) Subrouter() http.Handler {
|
||||
r := chi.NewMux()
|
||||
|
||||
r.Mount("/talkgroup", new(talkgroupAPI).Subrouter())
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
type errResponse struct {
|
||||
Err error `json:"-"`
|
||||
Code int `json:"-"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
render.Status(r, e.Code)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func badRequest(err error) render.Renderer {
|
||||
return &errResponse{
|
||||
Err: err,
|
||||
Code: http.StatusBadRequest,
|
||||
Error: "Bad request",
|
||||
}
|
||||
}
|
||||
|
||||
func recordNotFound(err error) render.Renderer {
|
||||
return &errResponse{
|
||||
Err: err,
|
||||
Code: http.StatusNotFound,
|
||||
Error: "Record not found",
|
||||
}
|
||||
}
|
||||
|
||||
func internalError(err error) render.Renderer {
|
||||
return &errResponse{
|
||||
Err: err,
|
||||
Code: http.StatusNotFound,
|
||||
Error: "Internal server error",
|
||||
}
|
||||
}
|
||||
|
||||
type errResponder func(error) render.Renderer
|
||||
|
||||
var statusMapping = map[error]errResponder{
|
||||
talkgroups.ErrNoSuchSystem: recordNotFound,
|
||||
talkgroups.ErrNotFound: recordNotFound,
|
||||
pgx.ErrNoRows: recordNotFound,
|
||||
}
|
||||
|
||||
func autoError(err error) render.Renderer {
|
||||
c, ok := statusMapping[err]
|
||||
if ok {
|
||||
c(err)
|
||||
}
|
||||
|
||||
for e, c := range statusMapping { // check if err wraps an error we know about
|
||||
if errors.Is(err, e) {
|
||||
return c(err)
|
||||
}
|
||||
}
|
||||
|
||||
return internalError(err)
|
||||
}
|
||||
|
||||
func wErr(w http.ResponseWriter, r *http.Request, v render.Renderer) {
|
||||
err := render.Render(w, r, v)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("wErr render error")
|
||||
}
|
||||
}
|
||||
|
||||
func decodeParams(d interface{}, r *http.Request) error {
|
||||
params := chi.RouteContext(r.Context()).URLParams
|
||||
m := make(map[string]string, len(params.Keys))
|
||||
|
||||
for i, k := range params.Keys {
|
||||
m[k] = params.Values[i]
|
||||
}
|
||||
|
||||
dec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
|
||||
Metadata: nil,
|
||||
Result: d,
|
||||
TagName: "param",
|
||||
WeaklyTypedInput: true,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return dec.Decode(m)
|
||||
}
|
||||
|
||||
func respond(w http.ResponseWriter, r *http.Request, v interface{}) {
|
||||
render.DefaultResponder(w, r, v)
|
||||
}
|
125
pkg/rest/talkgroups.go
Normal file
125
pkg/rest/talkgroups.go
Normal file
|
@ -0,0 +1,125 @@
|
|||
package rest
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
type talkgroupAPI struct {
|
||||
}
|
||||
|
||||
func (tga *talkgroupAPI) Subrouter() http.Handler {
|
||||
r := chi.NewMux()
|
||||
|
||||
r.Get("/{system:\\d+}/{id:\\d+}", tga.get)
|
||||
r.Put("/{system:\\d+}/{id:\\d+}", tga.put)
|
||||
r.Get("/{system:\\d+}/", tga.get)
|
||||
r.Get("/", tga.get)
|
||||
r.Post("/import", tga.tgImport)
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
type tgParams struct {
|
||||
System *int `param:"system"`
|
||||
ID *int `param:"id"`
|
||||
}
|
||||
|
||||
func (t tgParams) haveBoth() bool {
|
||||
return t.System != nil && t.ID != nil
|
||||
}
|
||||
|
||||
func (t tgParams) ToID() talkgroups.ID {
|
||||
nilOr := func(i *int) uint32 {
|
||||
if i == nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
return uint32(*i)
|
||||
}
|
||||
|
||||
return talkgroups.ID{
|
||||
System: nilOr(t.System),
|
||||
Talkgroup: nilOr(t.ID),
|
||||
}
|
||||
}
|
||||
|
||||
func (tga *talkgroupAPI) get(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
tgs := talkgroups.StoreFrom(ctx)
|
||||
|
||||
var p tgParams
|
||||
|
||||
err := decodeParams(&p, r)
|
||||
if err != nil {
|
||||
wErr(w, r, badRequest(err))
|
||||
return
|
||||
}
|
||||
|
||||
var res interface{}
|
||||
switch {
|
||||
case p.System != nil && p.ID != nil:
|
||||
res, err = tgs.TG(ctx, talkgroups.TG(*p.System, *p.ID))
|
||||
case p.System != nil:
|
||||
res, err = tgs.SystemTGs(ctx, int32(*p.System))
|
||||
default:
|
||||
res, err = tgs.TGs(ctx, nil)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
wErr(w, r, autoError(err))
|
||||
return
|
||||
}
|
||||
|
||||
respond(w, r, res)
|
||||
}
|
||||
|
||||
func (tga *talkgroupAPI) put(w http.ResponseWriter, r *http.Request) {
|
||||
var id tgParams
|
||||
err := decodeParams(&id, r)
|
||||
if err != nil {
|
||||
wErr(w, r, badRequest(err))
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
tgs := talkgroups.StoreFrom(ctx)
|
||||
|
||||
input := database.UpdateTalkgroupParams{}
|
||||
|
||||
err = forms.Unmarshal(r, &input, forms.WithTag("json"), forms.WithAcceptBlank(), forms.WithOmitEmpty())
|
||||
if err != nil {
|
||||
wErr(w, r, badRequest(err))
|
||||
return
|
||||
}
|
||||
|
||||
record, err := tgs.UpdateTG(ctx, input)
|
||||
if err != nil {
|
||||
wErr(w, r, autoError(err))
|
||||
return
|
||||
}
|
||||
|
||||
respond(w, r, record)
|
||||
}
|
||||
|
||||
func (tga *talkgroupAPI) tgImport(w http.ResponseWriter, r *http.Request) {
|
||||
var impJob importer.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(r.Context())
|
||||
if err != nil {
|
||||
wErr(w, r, autoError(err))
|
||||
return
|
||||
}
|
||||
|
||||
respond(w, r, recs)
|
||||
}
|
|
@ -4,14 +4,9 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"net/http/pprof"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
)
|
||||
|
||||
func (s *Server) installPprof() {
|
||||
r := s.r
|
||||
r.HandleFunc("/debug/pprof/", pprof.Index)
|
||||
r.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
|
||||
r.HandleFunc("/debug/pprof/profile", pprof.Profile)
|
||||
r.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
|
||||
r.HandleFunc("/debug/pprof/trace", pprof.Trace)
|
||||
s.r.Mount("/debug", middleware.Profiler())
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import (
|
|||
"dynatron.me/x/stillbox/internal/version"
|
||||
"dynatron.me/x/stillbox/pkg/config"
|
||||
"dynatron.me/x/stillbox/pkg/database"
|
||||
"dynatron.me/x/stillbox/pkg/talkgroups"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/go-chi/httprate"
|
||||
|
@ -26,7 +27,8 @@ func (s *Server) setupRoutes() {
|
|||
}
|
||||
|
||||
r := s.r
|
||||
r.Use(middleware.WithValue(database.DBCTXKeyValue, s.db))
|
||||
r.Use(middleware.WithValue(database.DBCtxKey, s.db))
|
||||
r.Use(middleware.WithValue(talkgroups.StoreCtxKey, s.tgs))
|
||||
|
||||
s.installPprof()
|
||||
|
||||
|
@ -36,7 +38,7 @@ func (s *Server) setupRoutes() {
|
|||
s.nex.PrivateRoutes(r)
|
||||
s.auth.PrivateRoutes(r)
|
||||
s.alerter.PrivateRoutes(r)
|
||||
r.Mount("/api", s.api.Subrouter())
|
||||
r.Mount("/api", s.rest.Subrouter())
|
||||
})
|
||||
|
||||
r.Group(func(r chi.Router) {
|
||||
|
|
|
@ -7,12 +7,12 @@ import (
|
|||
"time"
|
||||
|
||||
"dynatron.me/x/stillbox/pkg/alerting"
|
||||
"dynatron.me/x/stillbox/pkg/api"
|
||||
"dynatron.me/x/stillbox/pkg/auth"
|
||||
"dynatron.me/x/stillbox/pkg/config"
|
||||
"dynatron.me/x/stillbox/pkg/database"
|
||||
"dynatron.me/x/stillbox/pkg/nexus"
|
||||
"dynatron.me/x/stillbox/pkg/notify"
|
||||
"dynatron.me/x/stillbox/pkg/rest"
|
||||
"dynatron.me/x/stillbox/pkg/sinks"
|
||||
"dynatron.me/x/stillbox/pkg/sources"
|
||||
"dynatron.me/x/stillbox/pkg/talkgroups"
|
||||
|
@ -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
|
||||
|
@ -37,7 +37,7 @@ type Server struct {
|
|||
notifier notify.Notifier
|
||||
hup chan os.Signal
|
||||
tgs talkgroups.Store
|
||||
api api.API
|
||||
rest rest.API
|
||||
}
|
||||
|
||||
func New(ctx context.Context, cfg *config.Config) (*Server, error) {
|
||||
|
@ -61,7 +61,7 @@ func New(ctx context.Context, cfg *config.Config) (*Server, error) {
|
|||
}
|
||||
|
||||
tgCache := talkgroups.NewCache()
|
||||
api := api.New(tgCache)
|
||||
api := rest.New()
|
||||
|
||||
srv := &Server{
|
||||
auth: authenticator,
|
||||
|
@ -73,7 +73,7 @@ func New(ctx context.Context, cfg *config.Config) (*Server, error) {
|
|||
alerter: alerting.New(cfg.Alerting, tgCache, alerting.WithNotifier(notifier)),
|
||||
notifier: notifier,
|
||||
tgs: tgCache,
|
||||
api: api,
|
||||
rest: api,
|
||||
}
|
||||
|
||||
srv.sinks.Register("database", sinks.NewDatabaseSink(srv.db), true)
|
||||
|
@ -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()
|
||||
|
||||
|
|
|
@ -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}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,86 +0,0 @@
|
|||
package talkgroups
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"dynatron.me/x/stillbox/internal/ruletime"
|
||||
)
|
||||
|
||||
type AlertConfig struct {
|
||||
sync.RWMutex
|
||||
m map[ID][]AlertRule
|
||||
}
|
||||
|
||||
type AlertRule struct {
|
||||
Times []ruletime.RuleTime `json:"times"`
|
||||
ScoreMultiplier float32 `json:"mult"`
|
||||
}
|
||||
|
||||
func NewAlertConfig() AlertConfig {
|
||||
return AlertConfig{
|
||||
m: make(map[ID][]AlertRule),
|
||||
}
|
||||
}
|
||||
|
||||
func (ac *AlertConfig) GetRules(tg ID) []AlertRule {
|
||||
ac.RLock()
|
||||
defer ac.RUnlock()
|
||||
|
||||
return ac.m[tg]
|
||||
}
|
||||
|
||||
func (ac *AlertConfig) UnmarshalTGRules(tg ID, confBytes []byte) error {
|
||||
ac.Lock()
|
||||
defer ac.Unlock()
|
||||
|
||||
if len(confBytes) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var rules []AlertRule
|
||||
err := json.Unmarshal(confBytes, &rules)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ac.m[tg] = rules
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ac *AlertConfig) ApplyAlertRules(id ID, t time.Time, coversOpts ...ruletime.CoversOption) float64 {
|
||||
ac.RLock()
|
||||
s, has := ac.m[id]
|
||||
ac.RUnlock()
|
||||
if !has {
|
||||
return 1.0
|
||||
}
|
||||
|
||||
final := 1.0
|
||||
|
||||
for _, ar := range s {
|
||||
if ar.MatchTime(t, coversOpts...) {
|
||||
final *= float64(ar.ScoreMultiplier)
|
||||
}
|
||||
}
|
||||
|
||||
return final
|
||||
}
|
||||
|
||||
func (ac *AlertConfig) Invalidate() {
|
||||
ac.Lock()
|
||||
defer ac.Unlock()
|
||||
|
||||
clear(ac.m)
|
||||
}
|
||||
|
||||
func (ar *AlertRule) MatchTime(t time.Time, coversOpts ...ruletime.CoversOption) bool {
|
||||
for _, at := range ar.Times {
|
||||
if at.Covers(t, coversOpts...) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
|
@ -6,8 +6,6 @@ import (
|
|||
"sync"
|
||||
"time"
|
||||
|
||||
"dynatron.me/x/stillbox/internal/ruletime"
|
||||
|
||||
"dynatron.me/x/stillbox/pkg/config"
|
||||
"dynatron.me/x/stillbox/pkg/database"
|
||||
|
||||
|
@ -17,7 +15,15 @@ import (
|
|||
|
||||
type tgMap map[ID]*Talkgroup
|
||||
|
||||
var (
|
||||
ErrNotFound = errors.New("talkgroup not found")
|
||||
ErrNoSuchSystem = errors.New("no such system")
|
||||
)
|
||||
|
||||
type Store interface {
|
||||
// UpdateTG updates a talkgroup record.
|
||||
UpdateTG(ctx context.Context, input database.UpdateTalkgroupParams) (*Talkgroup, error)
|
||||
|
||||
// TG retrieves a Talkgroup from the Store.
|
||||
TG(ctx context.Context, tg ID) (*Talkgroup, error)
|
||||
|
||||
|
@ -30,14 +36,11 @@ type Store interface {
|
|||
// SystemName retrieves a system name from the store. It returns the record and whether one was found.
|
||||
SystemName(ctx context.Context, id int) (string, bool)
|
||||
|
||||
// ApplyAlertRules applies the score's talkgroup alert rules to the call occurring at t and returns the weighted score.
|
||||
ApplyAlertRules(id ID, t time.Time, coversOpts ...ruletime.CoversOption) float64
|
||||
|
||||
// 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 []int64) 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()
|
||||
|
@ -49,16 +52,16 @@ type Store interface {
|
|||
HUP(*config.Config)
|
||||
}
|
||||
|
||||
type CtxStoreKeyT string
|
||||
type storeCtxKey string
|
||||
|
||||
const CtxStoreKey CtxStoreKeyT = "store"
|
||||
const StoreCtxKey storeCtxKey = "store"
|
||||
|
||||
func CtxWithStore(ctx context.Context, s Store) context.Context {
|
||||
return context.WithValue(ctx, CtxStoreKey, s)
|
||||
return context.WithValue(ctx, StoreCtxKey, s)
|
||||
}
|
||||
|
||||
func StoreFrom(ctx context.Context) Store {
|
||||
s, ok := ctx.Value(CtxStoreKey).(Store)
|
||||
s, ok := ctx.Value(StoreCtxKey).(Store)
|
||||
if !ok {
|
||||
return NewCache()
|
||||
}
|
||||
|
@ -75,12 +78,10 @@ func (t *cache) Invalidate() {
|
|||
defer t.Unlock()
|
||||
clear(t.tgs)
|
||||
clear(t.systems)
|
||||
t.AlertConfig.Invalidate()
|
||||
}
|
||||
|
||||
type cache struct {
|
||||
sync.RWMutex
|
||||
AlertConfig
|
||||
tgs tgMap
|
||||
systems map[int32]string
|
||||
}
|
||||
|
@ -88,9 +89,8 @@ type cache struct {
|
|||
// NewCache returns a new cache Store.
|
||||
func NewCache() Store {
|
||||
tgc := &cache{
|
||||
tgs: make(tgMap),
|
||||
systems: make(map[int32]string),
|
||||
AlertConfig: NewAlertConfig(),
|
||||
tgs: make(tgMap),
|
||||
systems: make(map[int32]string),
|
||||
}
|
||||
|
||||
return tgc
|
||||
|
@ -98,19 +98,20 @@ func NewCache() Store {
|
|||
|
||||
func (t *cache) Hint(ctx context.Context, tgs []ID) error {
|
||||
t.RLock()
|
||||
var toLoad []int64
|
||||
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.Pack())
|
||||
toLoad.Append(tg.System, tg.Talkgroup)
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
toLoad = make([]int64, 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.Pack())
|
||||
toLoad.Append(g.System, g.Talkgroup)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -127,16 +128,16 @@ 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
|
||||
|
||||
return t.AlertConfig.UnmarshalTGRules(tg, rec.Talkgroup.AlertConfig)
|
||||
return nil
|
||||
}
|
||||
|
||||
type row interface {
|
||||
database.GetTalkgroupsWithLearnedByPackedIDsRow | database.GetTalkgroupsWithLearnedRow |
|
||||
database.GetTalkgroupsWithLearnedBySystemRow
|
||||
database.GetTalkgroupsRow | database.GetTalkgroupsWithLearnedRow |
|
||||
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.Packed())
|
||||
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 []int64) 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
|
||||
}
|
||||
|
@ -213,8 +214,6 @@ func (t *cache) Load(ctx context.Context, tgs []int64) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
var ErrNotFound = errors.New("talkgroup not found")
|
||||
|
||||
func (t *cache) Weight(ctx context.Context, id ID, tm time.Time) float64 {
|
||||
tg, err := t.TG(ctx, id)
|
||||
if err != nil {
|
||||
|
@ -223,7 +222,7 @@ func (t *cache) Weight(ctx context.Context, id ID, tm time.Time) float64 {
|
|||
|
||||
m := float64(tg.Weight)
|
||||
|
||||
m *= t.AlertConfig.ApplyAlertRules(id, tm)
|
||||
m *= tg.AlertConfig.Apply(tm)
|
||||
|
||||
return float64(m)
|
||||
}
|
||||
|
@ -247,7 +246,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()})
|
||||
record, err := database.FromCtx(ctx).GetTalkgroupWithLearned(ctx, int32(tg.System), int32(tg.Talkgroup))
|
||||
switch err {
|
||||
case nil:
|
||||
case pgx.ErrNoRows:
|
||||
|
@ -257,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) {
|
||||
|
@ -290,3 +285,23 @@ func (t *cache) SystemName(ctx context.Context, id int) (name string, has bool)
|
|||
|
||||
return n, has
|
||||
}
|
||||
|
||||
func (t *cache) UpdateTG(ctx context.Context, input database.UpdateTalkgroupParams) (*Talkgroup, error) {
|
||||
sysName, has := t.SystemName(ctx, int(*input.SystemID))
|
||||
if !has {
|
||||
return nil, ErrNoSuchSystem
|
||||
}
|
||||
|
||||
tg, err := database.FromCtx(ctx).UpdateTalkgroup(ctx, input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
record := &Talkgroup{
|
||||
Talkgroup: tg,
|
||||
System: database.System{ID: int(tg.SystemID), Name: sysName},
|
||||
}
|
||||
t.add(record)
|
||||
|
||||
return record, nil
|
||||
}
|
||||
|
|
139
pkg/talkgroups/importer/import.go
Normal file
139
pkg/talkgroups/importer/import.go
Normal file
|
@ -0,0 +1,139 @@
|
|||
package importer
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"dynatron.me/x/stillbox/internal/jsontypes"
|
||||
"dynatron.me/x/stillbox/pkg/database"
|
||||
"dynatron.me/x/stillbox/pkg/talkgroups"
|
||||
)
|
||||
|
||||
type ImportSource string
|
||||
|
||||
const (
|
||||
ImportSrcRadioReference ImportSource = "radioreference"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrBadImportType = errors.New("unknown import type")
|
||||
)
|
||||
|
||||
type importer interface {
|
||||
importTalkgroups(ctx context.Context, sys int, r io.Reader) ([]talkgroups.Talkgroup, error)
|
||||
}
|
||||
|
||||
type ImportJob struct {
|
||||
Type ImportSource `json:"type"`
|
||||
SystemID int `json:"systemID"`
|
||||
Body string `json:"body"`
|
||||
|
||||
importer `json:"-"`
|
||||
}
|
||||
|
||||
func (ij *ImportJob) Import(ctx context.Context) ([]talkgroups.Talkgroup, error) {
|
||||
r := bytes.NewReader([]byte(ij.Body))
|
||||
|
||||
switch ij.Type {
|
||||
case ImportSrcRadioReference:
|
||||
ij.importer = new(radioReferenceImporter)
|
||||
default:
|
||||
return nil, ErrBadImportType
|
||||
}
|
||||
|
||||
return ij.importTalkgroups(ctx, 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(ctx context.Context, sys int, r io.Reader) ([]talkgroups.Talkgroup, error) {
|
||||
sc := bufio.NewScanner(r)
|
||||
tgs := make([]talkgroups.Talkgroup, 0, 8)
|
||||
sysn, has := talkgroups.StoreFrom(ctx).SystemName(ctx, sys)
|
||||
if !has {
|
||||
return nil, talkgroups.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 {
|
||||
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 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]}
|
||||
gn := groupName // must take a copy
|
||||
tgs = append(tgs, talkgroups.Talkgroup{
|
||||
Talkgroup: database.Talkgroup{
|
||||
ID: uuid.New(),
|
||||
TGID: int32(tgt.Talkgroup),
|
||||
SystemID: int32(tgt.System),
|
||||
Name: &fields[4],
|
||||
AlphaTag: &fields[3],
|
||||
TgGroup: &gn,
|
||||
Metadata: metadata,
|
||||
Tags: tags,
|
||||
Weight: 1.0,
|
||||
},
|
||||
System: database.System{
|
||||
ID: sys,
|
||||
Name: sysn,
|
||||
},
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if err := sc.Err(); err != nil {
|
||||
return tgs, err
|
||||
}
|
||||
|
||||
return tgs, nil
|
||||
}
|
90
pkg/talkgroups/importer/import_test.go
Normal file
90
pkg/talkgroups/importer/import_test.go
Normal file
|
@ -0,0 +1,90 @@
|
|||
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/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 {
|
||||
fixt, err := os.ReadFile("testdata/" + fixture)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return fixt
|
||||
}
|
||||
|
||||
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: "radioreference",
|
||||
impType: "radioreference",
|
||||
input: getFixture("riscon.txt"),
|
||||
jsExpect: getFixture("riscon.json"),
|
||||
sysID: 197,
|
||||
sysName: "RISCON",
|
||||
},
|
||||
{
|
||||
name: "unknown importer",
|
||||
impType: "nonexistent",
|
||||
expectErr: importer.ErrBadImportType,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
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: importer.ImportSource(tc.impType),
|
||||
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)
|
||||
|
||||
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, fixt, tgs)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
1
pkg/talkgroups/importer/testdata/riscon.json
vendored
Normal file
1
pkg/talkgroups/importer/testdata/riscon.json
vendored
Normal file
File diff suppressed because one or more lines are too long
408
pkg/talkgroups/importer/testdata/riscon.txt
vendored
Normal file
408
pkg/talkgroups/importer/testdata/riscon.txt
vendored
Normal file
|
@ -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
|
|
@ -12,6 +12,8 @@ type Talkgroup struct {
|
|||
Learned bool `json:"learned"`
|
||||
}
|
||||
|
||||
type Metadata map[string]interface{}
|
||||
|
||||
type Names struct {
|
||||
System string
|
||||
Talkgroup string
|
||||
|
@ -24,13 +26,16 @@ type ID struct {
|
|||
|
||||
type IDs []ID
|
||||
|
||||
func (ids *IDs) Packed() []int64 {
|
||||
r := make([]int64, len(*ids))
|
||||
for i := range *ids {
|
||||
r[i] = (*ids)[i].Pack()
|
||||
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 {
|
||||
|
@ -44,11 +49,6 @@ 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 (t ID) String() string {
|
||||
return fmt.Sprintf("%d:%d", t.System, t.Talkgroup)
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
@ -74,9 +56,8 @@ CREATE TABLE IF NOT EXISTS talkgroups_learned(
|
|||
CREATE TABLE IF NOT EXISTS alerts(
|
||||
id UUID PRIMARY KEY,
|
||||
time TIMESTAMPTZ NOT NULL,
|
||||
talkgroup INT8 REFERENCES talkgroups(id) NOT NULL,
|
||||
system_id INT4 REFERENCES systems(id) NOT NULL GENERATED ALWAYS AS (talkgroup >> 32) STORED,
|
||||
tgid INT4 NOT NULL GENERATED ALWAYS AS (talkgroup & x'ffffffff'::BIGINT) STORED,
|
||||
tgid INTEGER NOT NULL,
|
||||
system_id INTEGER REFERENCES systems(id) NOT NULL,
|
||||
weight REAL,
|
||||
score REAL,
|
||||
orig_score REAL,
|
||||
|
|
|
@ -41,12 +41,13 @@ source
|
|||
UPDATE calls SET transcript = $2 WHERE id = $1;
|
||||
|
||||
-- name: AddAlert :exec
|
||||
INSERT INTO alerts (id, time, talkgroup, weight, score, orig_score, notified, metadata)
|
||||
INSERT INTO alerts (id, time, tgid, system_id, weight, score, orig_score, notified, metadata)
|
||||
VALUES
|
||||
(
|
||||
sqlc.arg(id),
|
||||
sqlc.arg(time),
|
||||
sqlc.arg(packed_tg),
|
||||
sqlc.arg(tgid),
|
||||
sqlc.arg(system_id),
|
||||
sqlc.arg(weight),
|
||||
sqlc.arg(score),
|
||||
sqlc.arg(orig_score),
|
||||
|
|
|
@ -8,30 +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[@any_tags])
|
||||
AND (tags && ARRAY[@all_tags])
|
||||
AND NOT (tags @> ARRAY[@not_tags]);
|
||||
|
||||
-- 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 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, @tg_id);
|
||||
|
||||
-- name: GetTalkgroupWithLearned :one
|
||||
SELECT
|
||||
|
@ -39,35 +30,17 @@ 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) = (@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;
|
||||
|
||||
-- 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;
|
||||
WHERE tgl.system_id = @system_id AND tgl.tgid = @tgid AND ignored IS NOT TRUE;
|
||||
|
||||
-- name: GetTalkgroupsWithLearnedBySystem :many
|
||||
SELECT
|
||||
|
@ -78,7 +51,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 +68,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,
|
||||
|
@ -104,6 +77,20 @@ FROM talkgroups_learned tgl
|
|||
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
|
||||
SET
|
||||
name = COALESCE(sqlc.narg('name'), name),
|
||||
alpha_tag = COALESCE(sqlc.narg('alpha_tag'), alpha_tag),
|
||||
tg_group = COALESCE(sqlc.narg('tg_group'), tg_group),
|
||||
frequency = COALESCE(sqlc.narg('frequency'), frequency),
|
||||
metadata = COALESCE(sqlc.narg('metadata'), metadata),
|
||||
tags = COALESCE(sqlc.narg('tags'), tags),
|
||||
alert = COALESCE(sqlc.narg('alert'), alert),
|
||||
alert_config = COALESCE(sqlc.narg('alert_config'), alert_config),
|
||||
weight = COALESCE(sqlc.narg('weight'), weight)
|
||||
WHERE id = sqlc.narg('id') OR (system_id = sqlc.narg('system_id') AND tgid = sqlc.narg('tgid'))
|
||||
RETURNING *;
|
||||
|
|
|
@ -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"
|
||||
|
@ -27,3 +30,13 @@ sql:
|
|||
go_type: "time.Time"
|
||||
- db_type: "pg_catalog.text"
|
||||
go_type: "string"
|
||||
- column: "talkgroups.alert_config"
|
||||
go_type:
|
||||
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
|
||||
|
|
24
util/dumpdb.sh
Normal file
24
util/dumpdb.sh
Normal file
|
@ -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}"
|
Loading…
Reference in a new issue