This commit is contained in:
Daniel Ponte 2025-01-22 14:15:53 -05:00
parent 76a2214377
commit f1a5f70a79
12 changed files with 201 additions and 85 deletions

View file

@ -53,40 +53,6 @@ func (q *Queries) DeleteShare(ctx context.Context, id string) error {
return err return err
} }
const getIncidentTalkgroups = `-- name: GetIncidentTalkgroups :many
SELECT DISTINCT
c.system,
c.talkgroup
FROM incidents_calls ic
JOIN calls c ON (c.id = ic.call_id AND c.call_date = ic.call_date)
WHERE ic.incident_id = $1
`
type GetIncidentTalkgroupsRow struct {
System int `json:"system"`
Talkgroup int `json:"talkgroup"`
}
func (q *Queries) GetIncidentTalkgroups(ctx context.Context, incidentID uuid.UUID) ([]GetIncidentTalkgroupsRow, error) {
rows, err := q.db.Query(ctx, getIncidentTalkgroups, incidentID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetIncidentTalkgroupsRow
for rows.Next() {
var i GetIncidentTalkgroupsRow
if err := rows.Scan(&i.System, &i.Talkgroup); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getShare = `-- name: GetShare :one const getShare = `-- name: GetShare :one
SELECT SELECT
id, id,

View file

@ -10,6 +10,7 @@ import (
"dynatron.me/x/stillbox/internal/jsontypes" "dynatron.me/x/stillbox/internal/jsontypes"
"dynatron.me/x/stillbox/pkg/alerting/rules" "dynatron.me/x/stillbox/pkg/alerting/rules"
"github.com/google/uuid"
) )
const addLearnedTalkgroup = `-- name: AddLearnedTalkgroup :one const addLearnedTalkgroup = `-- name: AddLearnedTalkgroup :one
@ -117,6 +118,40 @@ func (q *Queries) GetAllTalkgroupTags(ctx context.Context) ([]string, error) {
return items, nil return items, nil
} }
const getIncidentTalkgroups = `-- name: GetIncidentTalkgroups :many
SELECT DISTINCT
c.system,
c.talkgroup
FROM incidents_calls ic
JOIN calls c ON (c.id = ic.call_id AND c.call_date = ic.call_date)
WHERE ic.incident_id = $1
`
type GetIncidentTalkgroupsRow struct {
System int `json:"system"`
Talkgroup int `json:"talkgroup"`
}
func (q *Queries) GetIncidentTalkgroups(ctx context.Context, incidentID uuid.UUID) ([]GetIncidentTalkgroupsRow, error) {
rows, err := q.db.Query(ctx, getIncidentTalkgroups, incidentID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetIncidentTalkgroupsRow
for rows.Next() {
var i GetIncidentTalkgroupsRow
if err := rows.Scan(&i.System, &i.Talkgroup); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getSystemName = `-- name: GetSystemName :one const getSystemName = `-- name: GetSystemName :one
SELECT name FROM systems WHERE id = $1 SELECT name FROM systems WHERE id = $1
` `

View file

@ -11,6 +11,7 @@ import (
"dynatron.me/x/stillbox/pkg/incidents" "dynatron.me/x/stillbox/pkg/incidents"
"dynatron.me/x/stillbox/pkg/rbac" "dynatron.me/x/stillbox/pkg/rbac"
"dynatron.me/x/stillbox/pkg/rbac/entities" "dynatron.me/x/stillbox/pkg/rbac/entities"
"dynatron.me/x/stillbox/pkg/talkgroups"
"dynatron.me/x/stillbox/pkg/users" "dynatron.me/x/stillbox/pkg/users"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5"
@ -53,9 +54,12 @@ type Store interface {
// CallIn returns whether an incident is in an call // CallIn returns whether an incident is in an call
CallIn(ctx context.Context, inc uuid.UUID, call uuid.UUID) (bool, error) CallIn(ctx context.Context, inc uuid.UUID, call uuid.UUID) (bool, error)
// TGsIn returns the talkgroups referenced by an incident as a map, primary for rbac use.
TGsIn(ctx context.Context, inc uuid.UUID) (talkgroups.PresenceMap, error)
} }
type store struct { type postgresStore struct {
} }
type storeCtxKey string type storeCtxKey string
@ -76,10 +80,10 @@ func FromCtx(ctx context.Context) Store {
} }
func NewStore() Store { func NewStore() Store {
return &store{} return &postgresStore{}
} }
func (s *store) CreateIncident(ctx context.Context, inc incidents.Incident) (*incidents.Incident, error) { func (s *postgresStore) CreateIncident(ctx context.Context, inc incidents.Incident) (*incidents.Incident, error) {
user, err := users.UserCheck(ctx, new(incidents.Incident), "create") user, err := users.UserCheck(ctx, new(incidents.Incident), "create")
if err != nil { if err != nil {
return nil, err return nil, err
@ -138,7 +142,7 @@ func (s *store) CreateIncident(ctx context.Context, inc incidents.Incident) (*in
return &inc, nil return &inc, nil
} }
func (s *store) AddRemoveIncidentCalls(ctx context.Context, incidentID uuid.UUID, addCallIDs []uuid.UUID, notes []byte, removeCallIDs []uuid.UUID) error { func (s *postgresStore) AddRemoveIncidentCalls(ctx context.Context, incidentID uuid.UUID, addCallIDs []uuid.UUID, notes []byte, removeCallIDs []uuid.UUID) error {
inc, err := s.Owner(ctx, incidentID) inc, err := s.Owner(ctx, incidentID)
if err != nil { if err != nil {
return err return err
@ -176,7 +180,7 @@ func (s *store) AddRemoveIncidentCalls(ctx context.Context, incidentID uuid.UUID
}, pgx.TxOptions{}) }, pgx.TxOptions{})
} }
func (s *store) Incidents(ctx context.Context, p IncidentsParams) (incs []Incident, totalCount int, err error) { func (s *postgresStore) Incidents(ctx context.Context, p IncidentsParams) (incs []Incident, totalCount int, err error) {
_, err = rbac.Check(ctx, new(incidents.Incident), rbac.WithActions(entities.ActionRead)) _, err = rbac.Check(ctx, new(incidents.Incident), rbac.WithActions(entities.ActionRead))
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err
@ -281,7 +285,7 @@ func fromDBCalls(d []database.GetIncidentCallsRow) []incidents.IncidentCall {
return r return r
} }
func (s *store) Incident(ctx context.Context, id uuid.UUID) (*incidents.Incident, error) { func (s *postgresStore) Incident(ctx context.Context, id uuid.UUID) (*incidents.Incident, error) {
_, err := rbac.Check(ctx, &incidents.Incident{ID: id}, rbac.WithActions(entities.ActionRead)) _, err := rbac.Check(ctx, &incidents.Incident{ID: id}, rbac.WithActions(entities.ActionRead))
if err != nil { if err != nil {
return nil, err return nil, err
@ -332,7 +336,7 @@ func (uip UpdateIncidentParams) toDBUIP(id uuid.UUID) database.UpdateIncidentPar
} }
} }
func (s *store) UpdateIncident(ctx context.Context, id uuid.UUID, p UpdateIncidentParams) (*incidents.Incident, error) { func (s *postgresStore) UpdateIncident(ctx context.Context, id uuid.UUID, p UpdateIncidentParams) (*incidents.Incident, error) {
ckinc, err := s.Owner(ctx, id) ckinc, err := s.Owner(ctx, id)
if err != nil { if err != nil {
return nil, err return nil, err
@ -355,7 +359,7 @@ func (s *store) UpdateIncident(ctx context.Context, id uuid.UUID, p UpdateIncide
return &inc, nil return &inc, nil
} }
func (s *store) DeleteIncident(ctx context.Context, id uuid.UUID) error { func (s *postgresStore) DeleteIncident(ctx context.Context, id uuid.UUID) error {
inc, err := s.Owner(ctx, id) inc, err := s.Owner(ctx, id)
if err != nil { if err != nil {
return err return err
@ -369,16 +373,39 @@ func (s *store) DeleteIncident(ctx context.Context, id uuid.UUID) error {
return database.FromCtx(ctx).DeleteIncident(ctx, id) return database.FromCtx(ctx).DeleteIncident(ctx, id)
} }
func (s *store) UpdateNotes(ctx context.Context, incidentID uuid.UUID, callID uuid.UUID, notes []byte) error { func (s *postgresStore) UpdateNotes(ctx context.Context, incidentID uuid.UUID, callID uuid.UUID, notes []byte) error {
return database.FromCtx(ctx).UpdateCallIncidentNotes(ctx, notes, incidentID, callID) return database.FromCtx(ctx).UpdateCallIncidentNotes(ctx, notes, incidentID, callID)
} }
func (s *store) Owner(ctx context.Context, id uuid.UUID) (incidents.Incident, error) { func (s *postgresStore) Owner(ctx context.Context, id uuid.UUID) (incidents.Incident, error) {
owner, err := database.FromCtx(ctx).GetIncidentOwner(ctx, id) owner, err := database.FromCtx(ctx).GetIncidentOwner(ctx, id)
return incidents.Incident{ID: id, Owner: users.UserID(owner)}, err return incidents.Incident{ID: id, Owner: users.UserID(owner)}, err
} }
func (s *store) CallIn(ctx context.Context, inc uuid.UUID, call uuid.UUID) (bool, error) { func (s *postgresStore) CallIn(ctx context.Context, inc uuid.UUID, call uuid.UUID) (bool, error) {
db := database.FromCtx(ctx) db := database.FromCtx(ctx)
return db.CallInIncident(ctx, inc, call) return db.CallInIncident(ctx, inc, call)
} }
func (s *postgresStore) TGsIn(ctx context.Context, id uuid.UUID) (talkgroups.PresenceMap, error) {
_, err := rbac.Check(ctx, &incidents.Incident{ID: id}, rbac.WithActions(entities.ActionRead))
if err != nil {
return nil, err
}
db := database.FromCtx(ctx)
tgs, err := db.GetIncidentTalkgroups(ctx, id)
if err != nil {
return nil, err
}
m := make(talkgroups.PresenceMap, len(tgs))
for _, t := range tgs {
m.Put(talkgroups.ID{
System: uint32(t.System),
Talkgroup: uint32(t.Talkgroup),
})
}
return m, nil
}

View file

@ -7,6 +7,7 @@ import (
"reflect" "reflect"
"dynatron.me/x/stillbox/pkg/incidents/incstore" "dynatron.me/x/stillbox/pkg/incidents/incstore"
"dynatron.me/x/stillbox/pkg/talkgroups"
"github.com/el-mike/restrict/v2" "github.com/el-mike/restrict/v2"
"github.com/google/uuid" "github.com/google/uuid"
@ -16,8 +17,58 @@ const (
SubmitterEqualConditionType = "SUBMITTER_EQUAL" SubmitterEqualConditionType = "SUBMITTER_EQUAL"
InMapConditionType = "IN_MAP" InMapConditionType = "IN_MAP"
CallInIncidentConditionType = "CALL_IN_INCIDENT" CallInIncidentConditionType = "CALL_IN_INCIDENT"
TGInIncidentConditionType = "TG_IN_INCIDENT"
) )
type TGInIncidentCondition struct {
ID string `json:"name,omitempty" yaml:"name,omitempty"`
TG *restrict.ValueDescriptor `json:"tg" yaml:"tg"`
Incident *restrict.ValueDescriptor `json:"incident" yaml:"incident"`
}
func (*TGInIncidentCondition) Type() string {
return TGInIncidentConditionType
}
func (c *TGInIncidentCondition) Check(r *restrict.AccessRequest) error {
tgVID, err := c.TG.GetValue(r)
if err != nil {
return err
}
incVID, err := c.Incident.GetValue(r)
if err != nil {
return err
}
ctx, hasCtx := r.Context["ctx"].(context.Context)
if !hasCtx {
return restrict.NewConditionNotSatisfiedError(c, r, fmt.Errorf("no context provided"))
}
incID, isUUID := incVID.(uuid.UUID)
if !isUUID {
return restrict.NewConditionNotSatisfiedError(c, r, errors.New("incident ID is not UUID"))
}
tgID, isTGID := tgVID.(talkgroups.ID)
if !isTGID {
return restrict.NewConditionNotSatisfiedError(c, r, errors.New("tg ID is not TGID"))
}
// XXX: this should instead come from the access request context, for better reuse upstream
tgm, err := incstore.FromCtx(ctx).TGsIn(ctx, incID)
if err != nil {
return restrict.NewConditionNotSatisfiedError(c, r, err)
}
if !tgm.Has(tgID) {
return restrict.NewConditionNotSatisfiedError(c, r, fmt.Errorf(`tg "%v" not in incident "%v"`, tgID, incID))
}
return nil
}
type CallInIncidentCondition struct { type CallInIncidentCondition struct {
ID string `json:"name,omitempty" yaml:"name,omitempty"` ID string `json:"name,omitempty" yaml:"name,omitempty"`
Call *restrict.ValueDescriptor `json:"call" yaml:"call"` Call *restrict.ValueDescriptor `json:"call" yaml:"call"`
@ -60,7 +111,7 @@ func (c *CallInIncidentCondition) Check(r *restrict.AccessRequest) error {
} }
if !inCall { if !inCall {
return restrict.NewConditionNotSatisfiedError(c, r, fmt.Errorf(`incident "%v" not in call "%v"`, incID, callID)) return restrict.NewConditionNotSatisfiedError(c, r, fmt.Errorf(`call "%v" not in incident "%v"`, callID, incID))
} }
return nil return nil

View file

@ -54,6 +54,7 @@ func New(baseURL url.URL) *api {
ShareRequestCallDL: s.calls.shareCallDLRoute, ShareRequestCallDL: s.calls.shareCallDLRoute,
ShareRequestIncident: s.incidents.getIncident, ShareRequestIncident: s.incidents.getIncident,
ShareRequestIncidentM3U: s.incidents.getCallsM3U, ShareRequestIncidentM3U: s.incidents.getCallsM3U,
ShareRequestTalkgroups: s.tgs.getTGsShareRoute,
}, },
) )

View file

@ -102,17 +102,17 @@ func (ca *callsAPI) getAudio(p getAudioParams, w http.ResponseWriter, r *http.Re
_, _ = w.Write(call.AudioBlob) _, _ = w.Write(call.AudioBlob)
} }
func (ca *callsAPI) shareCallRoute(id uuid.UUID, _ *shares.Share, w http.ResponseWriter, r *http.Request) { func (ca *callsAPI) shareCallRoute(id ID, _ *shares.Share, w http.ResponseWriter, r *http.Request) {
p := getAudioParams{ p := getAudioParams{
CallID: &id, CallID: common.PtrTo(id.(uuid.UUID)),
} }
ca.getAudio(p, w, r) ca.getAudio(p, w, r)
} }
func (ca *callsAPI) shareCallDLRoute(id uuid.UUID, _ *shares.Share, w http.ResponseWriter, r *http.Request) { func (ca *callsAPI) shareCallDLRoute(id ID, _ *shares.Share, w http.ResponseWriter, r *http.Request) {
p := getAudioParams{ p := getAudioParams{
CallID: &id, CallID: common.PtrTo(id.(uuid.UUID)),
Download: common.PtrTo("download"), Download: common.PtrTo("download"),
} }

View file

@ -97,10 +97,10 @@ func (ia *incidentsAPI) getIncidentRoute(w http.ResponseWriter, r *http.Request)
ia.getIncident(id, nil, w, r) ia.getIncident(id, nil, w, r)
} }
func (ia *incidentsAPI) getIncident(id uuid.UUID, share *shares.Share, w http.ResponseWriter, r *http.Request) { func (ia *incidentsAPI) getIncident(id ID, share *shares.Share, w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
incs := incstore.FromCtx(ctx) incs := incstore.FromCtx(ctx)
inc, err := incs.Incident(ctx, id) inc, err := incs.Incident(ctx, id.(uuid.UUID))
if err != nil { if err != nil {
wErr(w, r, autoError(err)) wErr(w, r, autoError(err))
return return
@ -198,12 +198,12 @@ func (ia *incidentsAPI) getCallsM3URoute(w http.ResponseWriter, r *http.Request)
ia.getCallsM3U(id, nil, w, r) ia.getCallsM3U(id, nil, w, r)
} }
func (ia *incidentsAPI) getCallsM3U(id uuid.UUID, share *shares.Share, w http.ResponseWriter, r *http.Request) { func (ia *incidentsAPI) getCallsM3U(id ID, share *shares.Share, w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
incs := incstore.FromCtx(ctx) incs := incstore.FromCtx(ctx)
tgst := tgstore.FromCtx(ctx) tgst := tgstore.FromCtx(ctx)
inc, err := incs.Incident(ctx, id) inc, err := incs.Incident(ctx, id.(uuid.UUID))
if err != nil { if err != nil {
wErr(w, r, autoError(err)) wErr(w, r, autoError(err))
return return
@ -214,7 +214,7 @@ func (ia *incidentsAPI) getCallsM3U(id uuid.UUID, share *shares.Share, w http.Re
callUrl := common.PtrTo(*ia.baseURL) callUrl := common.PtrTo(*ia.baseURL)
urlRoot := "/api/call" urlRoot := "/api/call"
if share != nil { if share != nil {
urlRoot = fmt.Sprintf("/share/%s/%s/call/", share.Type, share.ID) urlRoot = fmt.Sprintf("/share/%s/call/", share.ID)
} }
b.WriteString("#EXTM3U\n\n") b.WriteString("#EXTM3U\n\n")

View file

@ -25,11 +25,13 @@ const (
ShareRequestCallDL ShareRequestType = "callDL" ShareRequestCallDL ShareRequestType = "callDL"
ShareRequestIncident ShareRequestType = "incident" ShareRequestIncident ShareRequestType = "incident"
ShareRequestIncidentM3U ShareRequestType = "m3u" ShareRequestIncidentM3U ShareRequestType = "m3u"
ShareRequestTalkgroups ShareRequestType = "talkgroups"
) )
func (rt ShareRequestType) IsValid() bool { func (rt ShareRequestType) IsValid() bool {
switch rt { switch rt {
case ShareRequestCall, ShareRequestCallDL, ShareRequestIncident, ShareRequestIncidentM3U: case ShareRequestCall, ShareRequestCallDL, ShareRequestIncident,
ShareRequestIncidentM3U, ShareRequestTalkgroups:
return true return true
} }
@ -38,14 +40,17 @@ func (rt ShareRequestType) IsValid() bool {
func (rt ShareRequestType) IsValidSubtype() bool { func (rt ShareRequestType) IsValidSubtype() bool {
switch rt { switch rt {
case ShareRequestCall, ShareRequestCallDL: case ShareRequestCall, ShareRequestCallDL, ShareRequestTalkgroups:
return true return true
} }
return false return false
} }
type HandlerFunc func(id uuid.UUID, share *shares.Share, w http.ResponseWriter, r *http.Request) type ID interface {
}
type HandlerFunc func(id ID, share *shares.Share, w http.ResponseWriter, r *http.Request)
type ShareHandlers map[ShareRequestType]HandlerFunc type ShareHandlers map[ShareRequestType]HandlerFunc
type shareAPI struct { type shareAPI struct {
baseURL *url.URL baseURL *url.URL
@ -71,8 +76,8 @@ func (sa *shareAPI) Subrouter() http.Handler {
func (sa *shareAPI) RootRouter() http.Handler { func (sa *shareAPI) RootRouter() http.Handler {
r := chi.NewMux() r := chi.NewMux()
r.Get("/{type}/{shareId:[A-Za-z0-9_-]{20,}}", sa.routeShare) r.Get("/{shareId:[A-Za-z0-9_-]{20,}}/{type}", sa.routeShare)
r.Get("/{type}/{shareId:[A-Za-z0-9_-]{20,}}/{subType}/{subID}", sa.routeShare) r.Get("/{shareId:[A-Za-z0-9_-]{20,}}/{type}/{subID}", sa.routeShare)
return r return r
} }
@ -104,7 +109,6 @@ func (sa *shareAPI) routeShare(w http.ResponseWriter, r *http.Request) {
params := struct { params := struct {
Type string `param:"type"` Type string `param:"type"`
ID string `param:"shareId"` ID string `param:"shareId"`
SubType *string `param:"subType"`
SubID *string `param:"subID"` SubID *string `param:"subID"`
}{} }{}
@ -136,15 +140,11 @@ func (sa *shareAPI) routeShare(w http.ResponseWriter, r *http.Request) {
ctx = entities.CtxWithSubject(ctx, sh) ctx = entities.CtxWithSubject(ctx, sh)
r = r.WithContext(ctx) r = r.WithContext(ctx)
if params.SubType != nil { switch rType {
case ShareRequestTalkgroups:
sa.shnd[rType](nil, sh, w, r)
case ShareRequestCall, ShareRequestCallDL:
if params.SubID == nil { if params.SubID == nil {
// probably can't happen
wErr(w, r, autoError(ErrBadShare))
return
}
subT := ShareRequestType(*params.SubType)
if !subT.IsValidSubtype() {
wErr(w, r, autoError(ErrBadShare)) wErr(w, r, autoError(ErrBadShare))
return return
} }
@ -154,13 +154,11 @@ func (sa *shareAPI) routeShare(w http.ResponseWriter, r *http.Request) {
wErr(w, r, badRequest(err)) wErr(w, r, badRequest(err))
return return
} }
sa.shnd[rType](subIDU, sh, w, r)
sa.shnd[subT](subIDU, sh, w, r) case ShareRequestIncident, ShareRequestIncidentM3U:
return
}
sa.shnd[rType](sh.EntityID, sh, w, r) sa.shnd[rType](sh.EntityID, sh, w, r)
} }
}
func (sa *shareAPI) deleteShare(w http.ResponseWriter, r *http.Request) { func (sa *shareAPI) deleteShare(w http.ResponseWriter, r *http.Request) {
} }

View file

@ -7,6 +7,8 @@ import (
"dynatron.me/x/stillbox/internal/forms" "dynatron.me/x/stillbox/internal/forms"
"dynatron.me/x/stillbox/pkg/database" "dynatron.me/x/stillbox/pkg/database"
"dynatron.me/x/stillbox/pkg/incidents/incstore"
"dynatron.me/x/stillbox/pkg/shares"
"dynatron.me/x/stillbox/pkg/talkgroups" "dynatron.me/x/stillbox/pkg/talkgroups"
"dynatron.me/x/stillbox/pkg/talkgroups/tgstore" "dynatron.me/x/stillbox/pkg/talkgroups/tgstore"
"dynatron.me/x/stillbox/pkg/talkgroups/xport" "dynatron.me/x/stillbox/pkg/talkgroups/xport"
@ -159,6 +161,30 @@ func (tga *talkgroupAPI) postPaginated(w http.ResponseWriter, r *http.Request) {
respond(w, r, res) respond(w, r, res)
} }
func (tga *talkgroupAPI) getTGsShareRoute(_ ID, share *shares.Share, w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
tgs := tgstore.FromCtx(ctx)
tgIDs, err := incstore.FromCtx(ctx).TGsIn(ctx, share.EntityID)
if err != nil {
wErr(w, r, autoError(err))
return
}
idSl := make(talkgroups.IDs, 0, len(tgIDs))
for id := range tgIDs {
idSl = append(idSl, id)
}
tgRes, err := tgs.TGs(ctx, idSl)
if err != nil {
wErr(w, r, autoError(err))
return
}
respond(w, r, tgRes)
}
func (tga *talkgroupAPI) put(w http.ResponseWriter, r *http.Request) { func (tga *talkgroupAPI) put(w http.ResponseWriter, r *http.Request) {
var id tgParams var id tgParams
err := decodeParams(&id, r) err := decodeParams(&id, r)

View file

@ -41,6 +41,18 @@ type ID struct {
Talkgroup uint32 `json:"tg"` Talkgroup uint32 `json:"tg"`
} }
type PresenceMap map[ID]struct{}
func (t PresenceMap) Has(id ID) bool {
_, has := t[id]
return has
}
func (t PresenceMap) Put(id ID) {
t[id] = struct{}{}
}
var _ encoding.TextUnmarshaler = (*ID)(nil) var _ encoding.TextUnmarshaler = (*ID)(nil)
var ErrBadTG = errors.New("bad talkgroup format") var ErrBadTG = errors.New("bad talkgroup format")

View file

@ -24,11 +24,3 @@ DELETE FROM shares WHERE id = @id;
-- name: PruneShares :exec -- name: PruneShares :exec
DELETE FROM shares WHERE expiration < NOW(); DELETE FROM shares WHERE expiration < NOW();
-- name: GetIncidentTalkgroups :many
SELECT DISTINCT
c.system,
c.talkgroup
FROM incidents_calls ic
JOIN calls c ON (c.id = ic.call_id AND c.call_date = ic.call_date)
WHERE ic.incident_id = @incident_id;

View file

@ -281,3 +281,11 @@ INSERT INTO systems(id, name) VALUES(@id, @name);
-- name: DeleteSystem :exec -- name: DeleteSystem :exec
DELETE FROM systems WHERE id = @id; DELETE FROM systems WHERE id = @id;
-- name: GetIncidentTalkgroups :many
SELECT DISTINCT
c.system,
c.talkgroup
FROM incidents_calls ic
JOIN calls c ON (c.id = ic.call_id AND c.call_date = ic.call_date)
WHERE ic.incident_id = @incident_id;