diff --git a/internal/common/common.go b/internal/common/common.go index 01b6971..a558427 100644 --- a/internal/common/common.go +++ b/internal/common/common.go @@ -44,3 +44,12 @@ func PtrOrNull[T comparable](val T) *T { return &val } + +func ZeroOr[T any](v *T) T { + var zero T + if v == nil { + return zero + } + + return *v +} diff --git a/pkg/calls/filter.go b/pkg/calls/filter.go deleted file mode 100644 index 844c2e6..0000000 --- a/pkg/calls/filter.go +++ /dev/null @@ -1,113 +0,0 @@ -package calls - -import ( - "context" - - "dynatron.me/x/stillbox/pkg/database" - "dynatron.me/x/stillbox/pkg/pb" - - tgs "dynatron.me/x/stillbox/pkg/talkgroups" -) - -type TalkgroupFilter struct { - Talkgroups []tgs.ID `json:"talkgroups,omitempty"` - TalkgroupsNot []tgs.ID `json:"talkgroupsNot,omitempty"` - TalkgroupTagsAll []string `json:"talkgroupTagsAll,omitempty"` - TalkgroupTagsAny []string `json:"talkgroupTagsAny,omitempty"` - TalkgroupTagsNot []string `json:"talkgroupTagsNot,omitempty"` - - talkgroups map[tgs.ID]bool -} - -func TalkgroupFilterFromPB(ctx context.Context, p *pb.Filter) (*TalkgroupFilter, error) { - tgf := &TalkgroupFilter{ - TalkgroupTagsAll: p.TalkgroupTagsAll, - TalkgroupTagsAny: p.TalkgroupTagsAny, - TalkgroupTagsNot: p.TalkgroupTagsNot, - } - - if l := len(p.Talkgroups); l > 0 { - tgf.Talkgroups = make([]tgs.ID, l) - for i, t := range p.Talkgroups { - tgf.Talkgroups[i] = tgs.ID{ - System: uint32(t.System), - Talkgroup: uint32(t.Talkgroup), - } - } - } - - if l := len(p.TalkgroupsNot); l > 0 { - tgf.TalkgroupsNot = make([]tgs.ID, l) - for i, t := range p.TalkgroupsNot { - tgf.TalkgroupsNot[i] = tgs.ID{ - System: uint32(t.System), - Talkgroup: uint32(t.Talkgroup), - } - } - } - - return tgf, tgf.compile(ctx) -} - -func (f *TalkgroupFilter) hasTags() bool { - return len(f.TalkgroupTagsAny) > 0 || len(f.TalkgroupTagsAll) > 0 || len(f.TalkgroupTagsNot) > 0 -} - -func (f *TalkgroupFilter) GetFinalTalkgroups() map[tgs.ID]bool { - return f.talkgroups -} - -func (f *TalkgroupFilter) compile(ctx context.Context) error { - f.talkgroups = make(map[tgs.ID]bool) - for _, tg := range f.Talkgroups { - f.talkgroups[tg] = true - } - - if f.hasTags() { // don't bother with DB if no tags - db := database.FromCtx(ctx) - tagTGs, err := db.GetTalkgroupIDsByTags(ctx, f.TalkgroupTagsAny, f.TalkgroupTagsAll, f.TalkgroupTagsNot) - if err != nil { - return err - } - - for _, tg := range tagTGs { - f.talkgroups[tgs.ID{System: uint32(tg.SystemID), Talkgroup: uint32(tg.TGID)}] = true - } - } - - for _, tg := range f.TalkgroupsNot { - f.talkgroups[tg] = false - } - - return nil -} - -func (f *TalkgroupFilter) Test(ctx context.Context, call *Call) bool { - if f == nil { // no filter means all calls - return true - } - - if f.talkgroups == nil { - err := f.compile(ctx) - if err != nil { - panic(err) - } - } - - tg := call.TalkgroupTuple() - - tgRes, have := f.talkgroups[tg] - if have { - return tgRes - } - - for _, patch := range call.Patches { - tg.Talkgroup = uint32(patch) - tgRes, have := f.talkgroups[tg] - if have { - return tgRes - } - } - - return false -} diff --git a/pkg/nexus/client.go b/pkg/nexus/client.go index 61ff4d8..f6192ed 100644 --- a/pkg/nexus/client.go +++ b/pkg/nexus/client.go @@ -8,9 +8,9 @@ import ( "sync" "dynatron.me/x/stillbox/internal/version" - "dynatron.me/x/stillbox/pkg/calls" "dynatron.me/x/stillbox/pkg/database" "dynatron.me/x/stillbox/pkg/pb" + tgfilter "dynatron.me/x/stillbox/pkg/talkgroups/filter" "github.com/rs/zerolog/log" "google.golang.org/protobuf/proto" @@ -33,7 +33,7 @@ type client struct { Connection liveState pb.LiveState - filter *calls.TalkgroupFilter + filter *tgfilter.TalkgroupFilter nexus *Nexus } diff --git a/pkg/nexus/commands.go b/pkg/nexus/commands.go index 37e06a8..b134b33 100644 --- a/pkg/nexus/commands.go +++ b/pkg/nexus/commands.go @@ -3,9 +3,9 @@ package nexus import ( "context" - "dynatron.me/x/stillbox/pkg/calls" "dynatron.me/x/stillbox/pkg/pb" "dynatron.me/x/stillbox/pkg/talkgroups" + tgfilter "dynatron.me/x/stillbox/pkg/talkgroups/filter" "dynatron.me/x/stillbox/pkg/talkgroups/tgstore" "github.com/rs/zerolog/log" @@ -111,7 +111,7 @@ func (c *client) Live(ctx context.Context, cmd *pb.Live) error { } if cmd.Filter != nil { - filter, err := calls.TalkgroupFilterFromPB(ctx, cmd.Filter) + filter, err := tgfilter.TalkgroupFilterFromPB(ctx, cmd.Filter) if err != nil { log.Error().Err(err).Msg("filter create failed") return err diff --git a/pkg/rest/api.go b/pkg/rest/api.go index a55afdf..ceb014d 100644 --- a/pkg/rest/api.go +++ b/pkg/rest/api.go @@ -75,7 +75,6 @@ func errTextNotFound(err error) render.Renderer { } } - func internalError(err error) render.Renderer { return &errResponse{ Err: err, @@ -89,7 +88,7 @@ type errResponder func(error) render.Renderer var statusMapping = map[error]errResponder{ tgstore.ErrNoSuchSystem: errTextNotFound, tgstore.ErrNotFound: errTextNotFound, - pgx.ErrNoRows: recordNotFound, + pgx.ErrNoRows: recordNotFound, } func autoError(err error) render.Renderer { diff --git a/pkg/rest/talkgroups.go b/pkg/rest/talkgroups.go index e9c4fc5..e1147df 100644 --- a/pkg/rest/talkgroups.go +++ b/pkg/rest/talkgroups.go @@ -7,7 +7,7 @@ import ( "dynatron.me/x/stillbox/pkg/database" "dynatron.me/x/stillbox/pkg/talkgroups" "dynatron.me/x/stillbox/pkg/talkgroups/tgstore" - "dynatron.me/x/stillbox/pkg/talkgroups/importer" + "dynatron.me/x/stillbox/pkg/talkgroups/xport" "github.com/go-chi/chi/v5" ) @@ -114,7 +114,7 @@ func (tga *talkgroupAPI) put(w http.ResponseWriter, r *http.Request) { } func (tga *talkgroupAPI) tgImport(w http.ResponseWriter, r *http.Request) { - var impJob importer.ImportJob + var impJob xport.ImportJob err := forms.Unmarshal(r, &impJob, forms.WithTag("json"), forms.WithAcceptBlank(), forms.WithOmitEmpty()) if err != nil { wErr(w, r, badRequest(err)) diff --git a/pkg/sinks/database.go b/pkg/sinks/database.go index 98ea90c..e8529a4 100644 --- a/pkg/sinks/database.go +++ b/pkg/sinks/database.go @@ -15,7 +15,7 @@ import ( ) type DatabaseSink struct { - db database.Store + db database.Store tgs tgstore.Store } diff --git a/pkg/talkgroups/filter/filter.go b/pkg/talkgroups/filter/filter.go new file mode 100644 index 0000000..3072261 --- /dev/null +++ b/pkg/talkgroups/filter/filter.go @@ -0,0 +1,168 @@ +package filter + +import ( + "context" + + "dynatron.me/x/stillbox/pkg/calls" + "dynatron.me/x/stillbox/pkg/database" + "dynatron.me/x/stillbox/pkg/pb" + + tgsp "dynatron.me/x/stillbox/pkg/talkgroups" +) + +type TalkgroupFilter struct { + Talkgroups []tgsp.ID `json:"talkgroups,omitempty"` + TalkgroupsNot []tgsp.ID `json:"talkgroupsNot,omitempty"` + TalkgroupTagsAll []string `json:"talkgroupTagsAll,omitempty"` + TalkgroupTagsAny []string `json:"talkgroupTagsAny,omitempty"` + TalkgroupTagsNot []string `json:"talkgroupTagsNot,omitempty"` + + talkgroups map[tgsp.ID]bool `json:"-"` +} + +func (f *TalkgroupFilter) TGs(ctx context.Context) (tgsp.IDs, error) { + err := f.ensureCompiled(ctx) + if err != nil { + return nil, err + } + + r := make(tgsp.IDs, 0, len(f.talkgroups)) + for tg := range f.talkgroups { + r = append(r, tg) + } + + return r, nil +} + +func (f *TalkgroupFilter) Tuples(ctx context.Context) (database.TGTuples, error) { + err := f.ensureCompiled(ctx) + if err != nil { + return database.TGTuples{}, err + } + + sys := make([]uint32, len(f.talkgroups)) + tgs := make([]uint32, len(f.talkgroups)) + + i := 0 + for tg := range f.talkgroups { + sys[i] = tg.System + tgs[i] = tg.Talkgroup + } + + return database.TGTuples{sys, tgs}, nil +} + +func (f *TalkgroupFilter) ensureCompiled(ctx context.Context) error { + if f.talkgroups == nil { + return f.compile(ctx) + } + + return nil +} + +func (tgf *TalkgroupFilter) IsEmpty() bool { + if tgf == nil { + return true + } + + if len(tgf.Talkgroups) > 0 || + len(tgf.TalkgroupsNot) > 0 || + len(tgf.TalkgroupTagsAll) > 0 || + len(tgf.TalkgroupTagsAny) > 0 || + len(tgf.TalkgroupsNot) > 0 { + return false + } + + return true +} + +func TalkgroupFilterFromPB(ctx context.Context, p *pb.Filter) (*TalkgroupFilter, error) { + tgf := &TalkgroupFilter{ + TalkgroupTagsAll: p.TalkgroupTagsAll, + TalkgroupTagsAny: p.TalkgroupTagsAny, + TalkgroupTagsNot: p.TalkgroupTagsNot, + } + + if l := len(p.Talkgroups); l > 0 { + tgf.Talkgroups = make([]tgsp.ID, l) + for i, t := range p.Talkgroups { + tgf.Talkgroups[i] = tgsp.ID{ + System: uint32(t.System), + Talkgroup: uint32(t.Talkgroup), + } + } + } + + if l := len(p.TalkgroupsNot); l > 0 { + tgf.TalkgroupsNot = make([]tgsp.ID, l) + for i, t := range p.TalkgroupsNot { + tgf.TalkgroupsNot[i] = tgsp.ID{ + System: uint32(t.System), + Talkgroup: uint32(t.Talkgroup), + } + } + } + + return tgf, tgf.compile(ctx) +} + +func (f *TalkgroupFilter) hasTags() bool { + return len(f.TalkgroupTagsAny) > 0 || len(f.TalkgroupTagsAll) > 0 || len(f.TalkgroupTagsNot) > 0 +} + +func (f *TalkgroupFilter) GetFinalTalkgroups() map[tgsp.ID]bool { + return f.talkgroups +} + +func (f *TalkgroupFilter) compile(ctx context.Context) error { + f.talkgroups = make(map[tgsp.ID]bool) + for _, tg := range f.Talkgroups { + f.talkgroups[tg] = true + } + + if f.hasTags() { // don't bother with DB if no tags + db := database.FromCtx(ctx) + tagTGs, err := db.GetTalkgroupIDsByTags(ctx, f.TalkgroupTagsAny, f.TalkgroupTagsAll, f.TalkgroupTagsNot) + if err != nil { + return err + } + + for _, tg := range tagTGs { + f.talkgroups[tgsp.ID{System: uint32(tg.SystemID), Talkgroup: uint32(tg.TGID)}] = true + } + } + + for _, tg := range f.TalkgroupsNot { + f.talkgroups[tg] = false + } + + return nil +} + +func (f *TalkgroupFilter) Test(ctx context.Context, call *calls.Call) bool { + if f == nil { // no filter means all calls + return true + } + + err := f.ensureCompiled(ctx) + if err != nil { + panic(err) + } + + tg := call.TalkgroupTuple() + + tgRes, have := f.talkgroups[tg] + if have { + return tgRes + } + + for _, patch := range call.Patches { + tg.Talkgroup = uint32(patch) + tgRes, have := f.talkgroups[tg] + if have { + return tgRes + } + } + + return false +} diff --git a/pkg/talkgroups/tgstore/store.go b/pkg/talkgroups/tgstore/store.go index 9ec106f..ccef441 100644 --- a/pkg/talkgroups/tgstore/store.go +++ b/pkg/talkgroups/tgstore/store.go @@ -9,9 +9,9 @@ import ( "dynatron.me/x/stillbox/internal/common" "dynatron.me/x/stillbox/pkg/auth" + "dynatron.me/x/stillbox/pkg/calls" "dynatron.me/x/stillbox/pkg/config" "dynatron.me/x/stillbox/pkg/database" - "dynatron.me/x/stillbox/pkg/calls" tgsp "dynatron.me/x/stillbox/pkg/talkgroups" "github.com/jackc/pgx/v5" @@ -135,6 +135,15 @@ func (t *cache) Hint(ctx context.Context, tgs []tgsp.ID) error { return nil } +func (t *cache) get(id tgsp.ID) (*tgsp.Talkgroup, bool) { + t.RLock() + defer t.RUnlock() + + tg, has := t.tgs[id] + + return tg, has +} + func (t *cache) add(rec *tgsp.Talkgroup) { t.Lock() defer t.Unlock() @@ -176,16 +185,14 @@ func (t *cache) TGs(ctx context.Context, tgs tgsp.IDs) ([]*tgsp.Talkgroup, error var err error if tgs != nil { toGet := make(tgsp.IDs, 0, len(tgs)) - t.RLock() for _, id := range tgs { - rec, has := t.tgs[id] + rec, has := t.get(id) if has { r = append(r, rec) } else { toGet = append(toGet, id) } } - t.RUnlock() tgRecords, err := database.FromCtx(ctx).GetTalkgroupsWithLearnedBySysTGID(ctx, toGet.Tuples()) if err != nil { @@ -240,9 +247,7 @@ func (t *cache) SystemTGs(ctx context.Context, systemID int32) ([]*tgsp.Talkgrou } func (t *cache) TG(ctx context.Context, tg tgsp.ID) (*tgsp.Talkgroup, error) { - t.RLock() - rec, has := t.tgs[tg] - t.RUnlock() + rec, has := t.get(tg) if has { return rec, nil @@ -326,7 +331,7 @@ func (t *cache) LearnTG(ctx context.Context, c *calls.Call) (*tgsp.Talkgroup, er tg := &tgsp.Talkgroup{ Talkgroup: tgm, System: database.System{ - ID: c.System, + ID: c.System, Name: sys, }, Learned: tgm.Learned, @@ -361,7 +366,6 @@ func (t *cache) UpsertTGs(ctx context.Context, system int, input []database.Upse input[i].SystemID = int32(system) input[i].Learned = common.PtrTo(false) - } var oerr error @@ -375,8 +379,8 @@ func (t *cache) UpsertTGs(ctx context.Context, system int, input []database.Upse return } versionParams = append(versionParams, database.StoreTGVersionParams{ - SystemID: int32(system), - TGID: r.TGID, + SystemID: int32(system), + TGID: r.TGID, Submitter: auth.UIDFrom(ctx), }) tgs = append(tgs, &tgsp.Talkgroup{ diff --git a/pkg/talkgroups/xport/export.go b/pkg/talkgroups/xport/export.go new file mode 100644 index 0000000..4fdcda9 --- /dev/null +++ b/pkg/talkgroups/xport/export.go @@ -0,0 +1,54 @@ +package xport + +import ( + "context" + "io" + + "dynatron.me/x/stillbox/pkg/talkgroups" + "dynatron.me/x/stillbox/pkg/talkgroups/filter" + "dynatron.me/x/stillbox/pkg/talkgroups/tgstore" + "dynatron.me/x/stillbox/pkg/talkgroups/xport/sdrtrunk" +) + +type Exporter interface { + ExportTalkgroups(ctx context.Context, w io.Writer, tgs []*talkgroups.Talkgroup, tmpl []byte) error +} + +type ExportJob struct { + Type Format `json:"type"` + SystemID int `json:"systemID"` + Template []byte `json:"template"` + + filter.TalkgroupFilter + Exporter +} + +func (ej *ExportJob) Export(ctx context.Context, w io.Writer) error { + var tgs []*talkgroups.Talkgroup + var err error + tgst := tgstore.FromCtx(ctx) + if ej.TalkgroupFilter.IsEmpty() { + tgs, err = tgst.SystemTGs(ctx, int32(ej.SystemID)) + if err != nil { + return err + } + } else { + ids, err := ej.TalkgroupFilter.TGs(ctx) + if err != nil { + return err + } + tgs, err = tgst.TGs(ctx, ids) + if err != nil { + return err + } + } + + switch ej.Type { + case FormatSDRTrunk: + ej.Exporter = sdrtrunk.New() + default: + return ErrBadType + } + + return ej.ExportTalkgroups(ctx, w, tgs, ej.Template) +} diff --git a/pkg/talkgroups/xport/format.go b/pkg/talkgroups/xport/format.go new file mode 100644 index 0000000..94ce95c --- /dev/null +++ b/pkg/talkgroups/xport/format.go @@ -0,0 +1,16 @@ +package xport + +import ( + "errors" +) + +type Format string + +const ( + FormatRadioReference Format = "radioreference" + FormatSDRTrunk Format = "sdrtrunk" +) + +var ( + ErrBadType = errors.New("unknown format type") +) diff --git a/pkg/talkgroups/xport/import.go b/pkg/talkgroups/xport/import.go new file mode 100644 index 0000000..ed73a7d --- /dev/null +++ b/pkg/talkgroups/xport/import.go @@ -0,0 +1,35 @@ +package xport + +import ( + "bytes" + "context" + "io" + + "dynatron.me/x/stillbox/pkg/talkgroups" + "dynatron.me/x/stillbox/pkg/talkgroups/xport/radioref" +) + +type Importer interface { + ImportTalkgroups(ctx context.Context, sys int, r io.Reader) ([]talkgroups.Talkgroup, error) +} + +type ImportJob struct { + Type Format `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 FormatRadioReference: + ij.Importer = radioref.New() + default: + return nil, ErrBadType + } + + return ij.ImportTalkgroups(ctx, ij.SystemID, r) +} diff --git a/pkg/talkgroups/xport/import_test.go b/pkg/talkgroups/xport/import_test.go new file mode 100644 index 0000000..ad04a88 --- /dev/null +++ b/pkg/talkgroups/xport/import_test.go @@ -0,0 +1,51 @@ +package xport_test + +import ( + "context" + "testing" + + "dynatron.me/x/stillbox/pkg/talkgroups/xport" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestImport(t *testing.T) { + tests := []struct { + name string + impType string + input []byte + sysID int + sysName string + jsExpect []byte + expectErr error + }{ + { + name: "unknown importer", + impType: "nonexistent", + expectErr: xport.ErrBadType, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + ctx := context.Background() + ij := &xport.ImportJob{ + Type: xport.Format(tc.impType), + SystemID: tc.sysID, + Body: string(tc.input), + } + + _, 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) + + } + + }) + } +} diff --git a/pkg/talkgroups/importer/import.go b/pkg/talkgroups/xport/radioref/radioreference.go similarity index 66% rename from pkg/talkgroups/importer/import.go rename to pkg/talkgroups/xport/radioref/radioreference.go index 347c25b..946586a 100644 --- a/pkg/talkgroups/importer/import.go +++ b/pkg/talkgroups/xport/radioref/radioreference.go @@ -1,10 +1,8 @@ -package importer +package radioref import ( "bufio" - "bytes" "context" - "errors" "io" "regexp" "strconv" @@ -16,42 +14,10 @@ import ( "dynatron.me/x/stillbox/pkg/talkgroups/tgstore" ) -type ImportSource string +type Driver struct{} -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 { +func New() *Driver { + return new(Driver) } type rrState int @@ -64,7 +30,7 @@ const ( var rrRE = regexp.MustCompile(`DEC\s+HEX\s+Mode\s+Alpha Tag\s+Description\s+Tag`) -func (rr *radioReferenceImporter) importTalkgroups(ctx context.Context, sys int, r io.Reader) ([]talkgroups.Talkgroup, error) { +func (rr *Driver) ImportTalkgroups(ctx context.Context, sys int, r io.Reader) ([]talkgroups.Talkgroup, error) { sc := bufio.NewScanner(r) tgs := make([]talkgroups.Talkgroup, 0, 8) sysn, has := tgstore.FromCtx(ctx).SystemName(ctx, sys) diff --git a/pkg/talkgroups/importer/import_test.go b/pkg/talkgroups/xport/radioref/radioreference_test.go similarity index 84% rename from pkg/talkgroups/importer/import_test.go rename to pkg/talkgroups/xport/radioref/radioreference_test.go index b82bed9..c453e2f 100644 --- a/pkg/talkgroups/importer/import_test.go +++ b/pkg/talkgroups/xport/radioref/radioreference_test.go @@ -1,4 +1,4 @@ -package importer_test +package radioref_test import ( "context" @@ -15,8 +15,8 @@ import ( "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" "dynatron.me/x/stillbox/pkg/talkgroups/tgstore" + "dynatron.me/x/stillbox/pkg/talkgroups/xport" ) func getFixture(fixture string) []byte { @@ -28,7 +28,7 @@ func getFixture(fixture string) []byte { return fixt } -func TestImport(t *testing.T) { +func TestRadioRef(t *testing.T) { // this is for deterministic UUIDs uuid.SetRand(rand.New(rand.NewSource(1))) @@ -42,18 +42,13 @@ func TestImport(t *testing.T) { expectErr error }{ { - name: "radioreference", + name: "radioreference import", 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 { @@ -64,8 +59,8 @@ func TestImport(t *testing.T) { } ctx := database.CtxWithDB(context.Background(), dbMock) ctx = tgstore.CtxWithStore(ctx, tgstore.NewCache()) - ij := &importer.ImportJob{ - Type: importer.ImportSource(tc.impType), + ij := &xport.ImportJob{ + Type: xport.Format(tc.impType), SystemID: tc.sysID, Body: string(tc.input), } diff --git a/pkg/talkgroups/importer/testdata/riscon.json b/pkg/talkgroups/xport/radioref/testdata/riscon.json similarity index 100% rename from pkg/talkgroups/importer/testdata/riscon.json rename to pkg/talkgroups/xport/radioref/testdata/riscon.json diff --git a/pkg/talkgroups/importer/testdata/riscon.txt b/pkg/talkgroups/xport/radioref/testdata/riscon.txt similarity index 100% rename from pkg/talkgroups/importer/testdata/riscon.txt rename to pkg/talkgroups/xport/radioref/testdata/riscon.txt diff --git a/pkg/talkgroups/xport/sdrtrunk/sdrtrunk.go b/pkg/talkgroups/xport/sdrtrunk/sdrtrunk.go new file mode 100644 index 0000000..31a7e95 --- /dev/null +++ b/pkg/talkgroups/xport/sdrtrunk/sdrtrunk.go @@ -0,0 +1,128 @@ +package sdrtrunk + +import ( + "context" + "encoding/xml" + "io" + + "dynatron.me/x/stillbox/internal/common" + "dynatron.me/x/stillbox/pkg/talkgroups" +) + +type Playlist struct { + XMLName xml.Name `xml:"playlist"` + Version int `xml:"version,attr"` + Aliases []Alias `xml:"alias"` + Channels []Channel `xml:"channel,omitempty"` + Streams []Stream `xml:"stream,omitempty"` +} + +type Alias struct { + XMLName xml.Name `xml:"alias"` + Name string `xml:"name,attr,omitempty"` + Color int `xml:"color,attr,omitempty"` + Group string `xml:"group,attr,omitempty"` + IconName string `xml:"iconName,attr,omitempty"` + List string `xml:"list,attr,omitempty"` + IDs []ID `xml:"id"` +} + +func tgToAlias(tg *talkgroups.Talkgroup) Alias { + return Alias{ + XMLName: xml.Name{Local: "alias"}, + Name: common.ZeroOr(tg.Name), + Group: common.ZeroOr(tg.TGGroup), + List: "Stillbox", + IDs: []ID{ + ID{ + XMLName: xml.Name{Local: "id"}, + Type: "talkgroup", + Value: common.PtrTo(int(tg.TGID)), + }, + }, + } +} + +type ID struct { + XMLName xml.Name `xml:"id"` + Type string `xml:"type,attr"` + Priority *int `xml:"priority,attr,omitempty"` + Channel *string `xml:"channel,attr,omitempty"` + Protocol *string `xml:"protocol,attr,omitempty"` + Value *int `xml:"value,attr,omitempty"` + Min *int `xml:"min,attr,omitempty"` + Max *int `xml:"max,attr,omitempty"` +} + +type Channel struct { + XMLName xml.Name `xml:"channel"` + Name string `xml:"name,attr"` + System string `xml:"system,attr"` + Enabled bool `xml:"enabled,attr"` + Site string `xml:"site,attr"` + Order int `xml:"order,attr"` + + AliasListName string `xml:"alias_list_name"` + EventLogConfig EventLogConfig `xml:"event_log_configuration"` + SourceConfig SourceConfig `xml:"source_configuration"` + AuxDecodeConfig AuxDecodeConfig `xml:"aux_decode_configuration"` + DecodeConfig DecodeConfig `xml:"decode_configuration"` + RecordConfig RecordConfig `xml:"record_configuration"` +} + +type EventLogConfig struct { + EventLogConfig []byte `xml:",innerxml"` +} + +type SourceConfig struct { + SourceConfig []byte `xml:",innerxml"` +} + +type AuxDecodeConfig struct { + AuxDecodeConfig []byte `xml:",innerxml"` +} + +type DecodeConfig struct { + DecodeConfig []byte `xml:",innerxml"` +} + +type RecordConfig struct { + RecordConfig []byte `xml:",innerxml"` +} + +type Stream struct { + Attributes []xml.Attr `xml:",any,attr"` + Stream []byte `xml:",innerxml"` +} + +func New() *Driver { + return new(Driver) +} + +type Driver struct{} + +func (st *Driver) ExportTalkgroups(ctx context.Context, w io.Writer, tgs []*talkgroups.Talkgroup, tmpl []byte) error { + var pl Playlist + + if tmpl != nil { + err := xml.Unmarshal(tmpl, &pl) + if err != nil { + return err + } + + pl.Aliases = nil + } + + for _, tg := range tgs { + pl.Aliases = append(pl.Aliases, tgToAlias(tg)) + } + + enc := xml.NewEncoder(w) + enc.Indent("", " ") + err := enc.Encode(&pl) + if err != nil { + return err + } + + return enc.Close() +}