From b5af8b0878db0db000b1d28fb2d8a36af1504c15 Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Sun, 29 Dec 2024 09:30:24 -0500 Subject: [PATCH] Initial endpoints --- internal/forms/marshal_test.go | 5 +- internal/forms/unmarshal_test.go | 1 - internal/jsontypes/jsontime.go | 8 ++ pkg/calls/call.go | 38 +++-- pkg/database/calls.sql.go | 16 ++- pkg/database/incidents.sql.go | 106 +++++++++++--- pkg/database/mocks/Store.go | 214 +++++++++++++++++++-------- pkg/database/querier.go | 5 +- pkg/incidents/incident.go | 9 ++ pkg/incidents/incstore/store.go | 109 ++++++++++++-- pkg/pb/stillbox.pb.go | 223 ++++++++++++++--------------- pkg/pb/stillbox.proto | 5 +- pkg/rest/api.go | 16 +++ pkg/rest/incidents.go | 108 ++++++++++++-- pkg/sinks/relay.go | 2 +- pkg/sources/http.go | 1 - sql/postgres/queries/calls.sql | 17 ++- sql/postgres/queries/incidents.sql | 50 ++++++- 18 files changed, 676 insertions(+), 257 deletions(-) diff --git a/internal/forms/marshal_test.go b/internal/forms/marshal_test.go index 6004218..b4aa217 100644 --- a/internal/forms/marshal_test.go +++ b/internal/forms/marshal_test.go @@ -32,7 +32,7 @@ func call(url string, call *calls.Call) error { var buf bytes.Buffer body := multipart.NewWriter(&buf) - err := forms.Marshal(call, body) + err := forms.Marshal(call, body, forms.WithTag("json")) if err != nil { return fmt.Errorf("relay form parse: %w", err) } @@ -88,6 +88,8 @@ func TestMarshal(t *testing.T) { t.Run(tc.name, func(t *testing.T) { var serr error var called bool + + // setup request handler h := hand(func(w http.ResponseWriter, r *http.Request) { called = true serr = r.ParseMultipartForm(1024 * 1024 * 2) @@ -112,6 +114,7 @@ func TestMarshal(t *testing.T) { }) svr := httptest.NewServer(h) + // perform the request err := call(svr.URL, &tc.call) assert.True(t, called) assert.NoError(t, err) diff --git a/internal/forms/unmarshal_test.go b/internal/forms/unmarshal_test.go index b664cb8..ae1898f 100644 --- a/internal/forms/unmarshal_test.go +++ b/internal/forms/unmarshal_test.go @@ -33,7 +33,6 @@ type callUploadRequest struct { Key string `form:"key"` Patches []int `form:"patches"` Source int `form:"source"` - Sources []int `form:"sources"` System int `form:"system"` SystemLabel string `form:"systemLabel"` Talkgroup int `form:"talkgroup"` diff --git a/internal/jsontypes/jsontime.go b/internal/jsontypes/jsontime.go index 6658c40..b54dacb 100644 --- a/internal/jsontypes/jsontime.go +++ b/internal/jsontypes/jsontime.go @@ -28,6 +28,14 @@ func (t *Time) UnmarshalYAML(n *yaml.Node) error { return nil } +func TimePtrFromTSTZ(t pgtype.Timestamptz) *Time { + if t.Valid { + return (*Time)(&t.Time) + } + + return nil +} + func (t *Time) PGTypeTSTZ() pgtype.Timestamptz { if t == nil { return pgtype.Timestamptz{Valid: false} diff --git a/pkg/calls/call.go b/pkg/calls/call.go index dcb77a3..5c784cb 100644 --- a/pkg/calls/call.go +++ b/pkg/calls/call.go @@ -42,26 +42,25 @@ type CallAudio struct { } type Call struct { - ID uuid.UUID `form:"-"` - Audio []byte `form:"audio" filenameField:"AudioName"` - AudioName string `form:"audioName"` - AudioType string `form:"audioType"` - Duration CallDuration `form:"-"` - DateTime time.Time `form:"dateTime"` - Frequencies []int `form:"frequencies"` - Frequency int `form:"frequency"` - Patches []int `form:"patches"` - Source int `form:"source"` - Sources []int `form:"sources"` - System int `form:"system"` - Submitter *auth.UserID `form:"-"` - SystemLabel string `form:"systemLabel"` - Talkgroup int `form:"talkgroup"` - TalkgroupGroup *string `form:"talkgroupGroup"` - TalkgroupLabel *string `form:"talkgroupLabel"` - TGAlphaTag *string `form:"talkgroupTag"` // not 1:1 + ID uuid.UUID `json:"-"` + Audio []byte `json:"audio,omitempty" filenameField:"AudioName"` + AudioName string `json:"audioName,omitempty"` + AudioType string `json:"audioType,omitempty"` + Duration CallDuration `json:"-"` + DateTime time.Time `json:"dateTime,omitempty"` + Frequencies []int `json:"frequencies,omitempty"` + Frequency int `json:"frequency,omitempty"` + Patches []int `json:"patches,omitempty"` + Source int `json:"source,omitempty"` + System int `json:"system,omitempty"` + Submitter *auth.UserID `json:"-,omitempty"` + SystemLabel string `json:"systemLabel,omitempty"` + Talkgroup int `json:"talkgroup,omitempty"` + TalkgroupGroup *string `json:"talkgroupGroup,omitempty"` + TalkgroupLabel *string `json:"talkgroupLabel,omitempty"` + TGAlphaTag *string `json:"talkgroupTag,omitempty"` - shouldStore bool `form:"-"` + shouldStore bool `json:"-"` } func (c *Call) String() string { @@ -114,7 +113,6 @@ func (c *Call) ToPB() *pb.Call { Frequency: int64(c.Frequency), Frequencies: toInt64Slice(c.Frequencies), Patches: toInt32Slice(c.Patches), - Sources: toInt32Slice(c.Sources), Duration: c.Duration.MsInt32Ptr(), Audio: c.Audio, } diff --git a/pkg/database/calls.sql.go b/pkg/database/calls.sql.go index 7cf94e9..1129b5e 100644 --- a/pkg/database/calls.sql.go +++ b/pkg/database/calls.sql.go @@ -157,7 +157,21 @@ func (q *Queries) CleanupSweptCalls(ctx context.Context, rangeStart pgtype.Times } const getCallAudioByID = `-- name: GetCallAudioByID :one -SELECT call_date, audio_name, audio_type, audio_blob FROM calls WHERE id = $1 +SELECT + c.call_date, + c.audio_name, + c.audio_type, + c.audio_blob +FROM calls c +WHERE c.id = $1 +UNION +SELECT + sc.call_date, + sc.audio_name, + sc.audio_type, + sc.audio_blob +FROM swept_calls sc +WHERE sc.id = $1 ` type GetCallAudioByIDRow struct { diff --git a/pkg/database/incidents.sql.go b/pkg/database/incidents.sql.go index 0091d1f..5edab34 100644 --- a/pkg/database/incidents.sql.go +++ b/pkg/database/incidents.sql.go @@ -131,36 +131,85 @@ func (q *Queries) GetIncident(ctx context.Context, id uuid.UUID) (Incident, erro return i, err } -const incidentCalls = `-- name: IncidentCalls :many -SELECT -ic.incident_id, call_date, -ic.call_id, -ic.notes -FROM incidents_calls ic +const getIncidentCalls = `-- name: GetIncidentCalls :many +SELECT ic.call_id, ic.call_date, ic.notes, c.submitter, c.system, c.talkgroup, c.audio_name, c.duration, c.audio_type, c.audio_url, c.frequency, c.frequencies, c.patches, c.source, c.transcript +FROM incidents_calls ic, LATERAL ( + SELECT + ca.submitter, + ca.system, + ca.talkgroup, + ca.audio_name, + ca.duration, + ca.audio_type, + ca.audio_url, + ca.frequency, + ca.frequencies, + ca.patches, + ca.source, + ca.transcript + FROM calls ca WHERE ca.id = ic.calls_tbl_id AND ca.call_date = ic.call_date + UNION + SELECT + sc.submitter, + sc.system, + sc.talkgroup, + sc.audio_name, + sc.duration, + sc.audio_type, + sc.audio_url, + sc.frequency, + sc.frequencies, + sc.patches, + sc.source, + sc.transcript + FROM swept_calls sc WHERE sc.id = ic.swept_call_id +) c +WHERE ic.incident_id = $1 ` -type IncidentCallsRow struct { - IncidentID uuid.UUID `json:"incident_id"` - CallDate pgtype.Timestamptz `json:"call_date"` - CallID uuid.UUID `json:"call_id"` - Notes []byte `json:"notes"` +type GetIncidentCallsRow struct { + CallID uuid.UUID `json:"call_id"` + CallDate pgtype.Timestamptz `json:"call_date"` + Notes []byte `json:"notes"` + Submitter *int32 `json:"submitter"` + System int `json:"system"` + Talkgroup int `json:"talkgroup"` + AudioName *string `json:"audio_name"` + Duration *int32 `json:"duration"` + AudioType *string `json:"audio_type"` + AudioUrl *string `json:"audio_url"` + Frequency int `json:"frequency"` + Frequencies []int `json:"frequencies"` + Patches []int `json:"patches"` + Source int `json:"source"` + Transcript *string `json:"transcript"` } -// INCOMPLETE -func (q *Queries) IncidentCalls(ctx context.Context) ([]IncidentCallsRow, error) { - rows, err := q.db.Query(ctx, incidentCalls) +func (q *Queries) GetIncidentCalls(ctx context.Context, id uuid.UUID) ([]GetIncidentCallsRow, error) { + rows, err := q.db.Query(ctx, getIncidentCalls, id) if err != nil { return nil, err } defer rows.Close() - var items []IncidentCallsRow + var items []GetIncidentCallsRow for rows.Next() { - var i IncidentCallsRow + var i GetIncidentCallsRow if err := rows.Scan( - &i.IncidentID, - &i.CallDate, &i.CallID, + &i.CallDate, &i.Notes, + &i.Submitter, + &i.System, + &i.Talkgroup, + &i.AudioName, + &i.Duration, + &i.AudioType, + &i.AudioUrl, + &i.Frequency, + &i.Frequencies, + &i.Patches, + &i.Source, + &i.Transcript, ); err != nil { return nil, err } @@ -253,6 +302,27 @@ func (q *Queries) ListIncidentsP(ctx context.Context, arg ListIncidentsPParams) return items, nil } +const removeFromIncident = `-- name: RemoveFromIncident :exec +DELETE FROM incidents_calls ic +WHERE ic.incident_id = $1 AND ic.call_id = ANY($2::UUID[]) +` + +func (q *Queries) RemoveFromIncident(ctx context.Context, iD uuid.UUID, callIds []uuid.UUID) error { + _, err := q.db.Exec(ctx, removeFromIncident, iD, callIds) + return err +} + +const updateCallIncidentNotes = `-- name: UpdateCallIncidentNotes :exec +UPDATE incidents_Calls +SET notes = $1 +WHERE incident_id = $2 AND call_id = $3 +` + +func (q *Queries) UpdateCallIncidentNotes(ctx context.Context, notes []byte, incidentID uuid.UUID, callID uuid.UUID) error { + _, err := q.db.Exec(ctx, updateCallIncidentNotes, notes, incidentID, callID) + return err +} + const updateIncident = `-- name: UpdateIncident :one UPDATE incidents SET diff --git a/pkg/database/mocks/Store.go b/pkg/database/mocks/Store.go index 45cf1b7..c525f78 100644 --- a/pkg/database/mocks/Store.go +++ b/pkg/database/mocks/Store.go @@ -1377,6 +1377,65 @@ func (_c *Store_GetIncident_Call) RunAndReturn(run func(context.Context, uuid.UU return _c } +// GetIncidentCalls provides a mock function with given fields: ctx, id +func (_m *Store) GetIncidentCalls(ctx context.Context, id uuid.UUID) ([]database.GetIncidentCallsRow, error) { + ret := _m.Called(ctx, id) + + if len(ret) == 0 { + panic("no return value specified for GetIncidentCalls") + } + + var r0 []database.GetIncidentCallsRow + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, uuid.UUID) ([]database.GetIncidentCallsRow, error)); ok { + return rf(ctx, id) + } + if rf, ok := ret.Get(0).(func(context.Context, uuid.UUID) []database.GetIncidentCallsRow); ok { + r0 = rf(ctx, id) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]database.GetIncidentCallsRow) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, uuid.UUID) error); ok { + r1 = rf(ctx, id) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Store_GetIncidentCalls_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetIncidentCalls' +type Store_GetIncidentCalls_Call struct { + *mock.Call +} + +// GetIncidentCalls is a helper method to define mock.On call +// - ctx context.Context +// - id uuid.UUID +func (_e *Store_Expecter) GetIncidentCalls(ctx interface{}, id interface{}) *Store_GetIncidentCalls_Call { + return &Store_GetIncidentCalls_Call{Call: _e.mock.On("GetIncidentCalls", ctx, id)} +} + +func (_c *Store_GetIncidentCalls_Call) Run(run func(ctx context.Context, id uuid.UUID)) *Store_GetIncidentCalls_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(uuid.UUID)) + }) + return _c +} + +func (_c *Store_GetIncidentCalls_Call) Return(_a0 []database.GetIncidentCallsRow, _a1 error) *Store_GetIncidentCalls_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *Store_GetIncidentCalls_Call) RunAndReturn(run func(context.Context, uuid.UUID) ([]database.GetIncidentCallsRow, error)) *Store_GetIncidentCalls_Call { + _c.Call.Return(run) + return _c +} + // GetSystemName provides a mock function with given fields: ctx, systemID func (_m *Store) GetSystemName(ctx context.Context, systemID int) (string, error) { ret := _m.Called(ctx, systemID) @@ -2594,64 +2653,6 @@ func (_c *Store_InTx_Call) RunAndReturn(run func(context.Context, func(database. return _c } -// IncidentCalls provides a mock function with given fields: ctx -func (_m *Store) IncidentCalls(ctx context.Context) ([]database.IncidentCallsRow, error) { - ret := _m.Called(ctx) - - if len(ret) == 0 { - panic("no return value specified for IncidentCalls") - } - - var r0 []database.IncidentCallsRow - var r1 error - if rf, ok := ret.Get(0).(func(context.Context) ([]database.IncidentCallsRow, error)); ok { - return rf(ctx) - } - if rf, ok := ret.Get(0).(func(context.Context) []database.IncidentCallsRow); ok { - r0 = rf(ctx) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]database.IncidentCallsRow) - } - } - - if rf, ok := ret.Get(1).(func(context.Context) error); ok { - r1 = rf(ctx) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// Store_IncidentCalls_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IncidentCalls' -type Store_IncidentCalls_Call struct { - *mock.Call -} - -// IncidentCalls is a helper method to define mock.On call -// - ctx context.Context -func (_e *Store_Expecter) IncidentCalls(ctx interface{}) *Store_IncidentCalls_Call { - return &Store_IncidentCalls_Call{Call: _e.mock.On("IncidentCalls", ctx)} -} - -func (_c *Store_IncidentCalls_Call) Run(run func(ctx context.Context)) *Store_IncidentCalls_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context)) - }) - return _c -} - -func (_c *Store_IncidentCalls_Call) Return(_a0 []database.IncidentCallsRow, _a1 error) *Store_IncidentCalls_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *Store_IncidentCalls_Call) RunAndReturn(run func(context.Context) ([]database.IncidentCallsRow, error)) *Store_IncidentCalls_Call { - _c.Call.Return(run) - return _c -} - // ListCallsCount provides a mock function with given fields: ctx, arg func (_m *Store) ListCallsCount(ctx context.Context, arg database.ListCallsCountParams) (int64, error) { ret := _m.Called(ctx, arg) @@ -2885,6 +2886,54 @@ func (_c *Store_ListIncidentsP_Call) RunAndReturn(run func(context.Context, data return _c } +// RemoveFromIncident provides a mock function with given fields: ctx, iD, callIds +func (_m *Store) RemoveFromIncident(ctx context.Context, iD uuid.UUID, callIds []uuid.UUID) error { + ret := _m.Called(ctx, iD, callIds) + + if len(ret) == 0 { + panic("no return value specified for RemoveFromIncident") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, uuid.UUID, []uuid.UUID) error); ok { + r0 = rf(ctx, iD, callIds) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Store_RemoveFromIncident_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RemoveFromIncident' +type Store_RemoveFromIncident_Call struct { + *mock.Call +} + +// RemoveFromIncident is a helper method to define mock.On call +// - ctx context.Context +// - iD uuid.UUID +// - callIds []uuid.UUID +func (_e *Store_Expecter) RemoveFromIncident(ctx interface{}, iD interface{}, callIds interface{}) *Store_RemoveFromIncident_Call { + return &Store_RemoveFromIncident_Call{Call: _e.mock.On("RemoveFromIncident", ctx, iD, callIds)} +} + +func (_c *Store_RemoveFromIncident_Call) Run(run func(ctx context.Context, iD uuid.UUID, callIds []uuid.UUID)) *Store_RemoveFromIncident_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(uuid.UUID), args[2].([]uuid.UUID)) + }) + return _c +} + +func (_c *Store_RemoveFromIncident_Call) Return(_a0 error) *Store_RemoveFromIncident_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Store_RemoveFromIncident_Call) RunAndReturn(run func(context.Context, uuid.UUID, []uuid.UUID) error) *Store_RemoveFromIncident_Call { + _c.Call.Return(run) + return _c +} + // RestoreTalkgroupVersion provides a mock function with given fields: ctx, versionIds func (_m *Store) RestoreTalkgroupVersion(ctx context.Context, versionIds int) (database.Talkgroup, error) { ret := _m.Called(ctx, versionIds) @@ -3244,6 +3293,55 @@ func (_c *Store_SweepCalls_Call) RunAndReturn(run func(context.Context, pgtype.T return _c } +// UpdateCallIncidentNotes provides a mock function with given fields: ctx, notes, incidentID, callID +func (_m *Store) UpdateCallIncidentNotes(ctx context.Context, notes []byte, incidentID uuid.UUID, callID uuid.UUID) error { + ret := _m.Called(ctx, notes, incidentID, callID) + + if len(ret) == 0 { + panic("no return value specified for UpdateCallIncidentNotes") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, []byte, uuid.UUID, uuid.UUID) error); ok { + r0 = rf(ctx, notes, incidentID, callID) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Store_UpdateCallIncidentNotes_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateCallIncidentNotes' +type Store_UpdateCallIncidentNotes_Call struct { + *mock.Call +} + +// UpdateCallIncidentNotes is a helper method to define mock.On call +// - ctx context.Context +// - notes []byte +// - incidentID uuid.UUID +// - callID uuid.UUID +func (_e *Store_Expecter) UpdateCallIncidentNotes(ctx interface{}, notes interface{}, incidentID interface{}, callID interface{}) *Store_UpdateCallIncidentNotes_Call { + return &Store_UpdateCallIncidentNotes_Call{Call: _e.mock.On("UpdateCallIncidentNotes", ctx, notes, incidentID, callID)} +} + +func (_c *Store_UpdateCallIncidentNotes_Call) Run(run func(ctx context.Context, notes []byte, incidentID uuid.UUID, callID uuid.UUID)) *Store_UpdateCallIncidentNotes_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].([]byte), args[2].(uuid.UUID), args[3].(uuid.UUID)) + }) + return _c +} + +func (_c *Store_UpdateCallIncidentNotes_Call) Return(_a0 error) *Store_UpdateCallIncidentNotes_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Store_UpdateCallIncidentNotes_Call) RunAndReturn(run func(context.Context, []byte, uuid.UUID, uuid.UUID) error) *Store_UpdateCallIncidentNotes_Call { + _c.Call.Return(run) + return _c +} + // UpdateIncident provides a mock function with given fields: ctx, arg func (_m *Store) UpdateIncident(ctx context.Context, arg database.UpdateIncidentParams) (database.Incident, error) { ret := _m.Called(ctx, arg) diff --git a/pkg/database/querier.go b/pkg/database/querier.go index 8997dce..f62194b 100644 --- a/pkg/database/querier.go +++ b/pkg/database/querier.go @@ -33,6 +33,7 @@ type Querier interface { GetCallAudioByID(ctx context.Context, id uuid.UUID) (GetCallAudioByIDRow, error) GetDatabaseSize(ctx context.Context) (string, error) GetIncident(ctx context.Context, id uuid.UUID) (Incident, error) + GetIncidentCalls(ctx context.Context, id uuid.UUID) ([]GetIncidentCallsRow, error) GetSystemName(ctx context.Context, systemID int) (string, error) GetTalkgroup(ctx context.Context, systemID int32, tGID int32) (GetTalkgroupRow, error) GetTalkgroupIDsByTags(ctx context.Context, anyTags []string, allTags []string, notTags []string) ([]GetTalkgroupIDsByTagsRow, error) @@ -50,12 +51,11 @@ type Querier interface { GetUserByUID(ctx context.Context, id int) (User, error) GetUserByUsername(ctx context.Context, username string) (User, error) GetUsers(ctx context.Context) ([]User, error) - // INCOMPLETE - IncidentCalls(ctx context.Context) ([]IncidentCallsRow, error) ListCallsCount(ctx context.Context, arg ListCallsCountParams) (int64, error) ListCallsP(ctx context.Context, arg ListCallsPParams) ([]ListCallsPRow, error) ListIncidentsCount(ctx context.Context, start pgtype.Timestamptz, end pgtype.Timestamptz) (int64, error) ListIncidentsP(ctx context.Context, arg ListIncidentsPParams) ([]Incident, error) + RemoveFromIncident(ctx context.Context, iD uuid.UUID, callIds []uuid.UUID) error RestoreTalkgroupVersion(ctx context.Context, versionIds int) (Talkgroup, error) SetAppPrefs(ctx context.Context, appName string, prefs []byte, uid int) error SetCallTranscript(ctx context.Context, iD uuid.UUID, transcript *string) error @@ -63,6 +63,7 @@ type Querier interface { StoreDeletedTGVersion(ctx context.Context, systemID *int32, tGID *int32, submitter *int32) error StoreTGVersion(ctx context.Context, arg []StoreTGVersionParams) *StoreTGVersionBatchResults SweepCalls(ctx context.Context, rangeStart pgtype.Timestamptz, rangeEnd pgtype.Timestamptz) (int64, error) + UpdateCallIncidentNotes(ctx context.Context, notes []byte, incidentID uuid.UUID, callID uuid.UUID) error UpdateIncident(ctx context.Context, arg UpdateIncidentParams) (Incident, error) UpdatePassword(ctx context.Context, username string, password string) error UpdateTalkgroup(ctx context.Context, arg UpdateTalkgroupParams) (Talkgroup, error) diff --git a/pkg/incidents/incident.go b/pkg/incidents/incident.go index 8f18e89..c2ee068 100644 --- a/pkg/incidents/incident.go +++ b/pkg/incidents/incident.go @@ -1,7 +1,10 @@ package incidents import ( + "encoding/json" + "dynatron.me/x/stillbox/internal/jsontypes" + "dynatron.me/x/stillbox/pkg/calls" "github.com/google/uuid" ) @@ -13,4 +16,10 @@ type Incident struct { EndTime *jsontypes.Time `json:"endTime"` Location jsontypes.Location `json:"location"` Metadata jsontypes.Metadata `json:"metadata"` + Calls []IncidentCall `json:"calls"` +} + +type IncidentCall struct { + calls.Call + Notes json.RawMessage `json:"notes"` } diff --git a/pkg/incidents/incstore/store.go b/pkg/incidents/incstore/store.go index 933d131..3bc0339 100644 --- a/pkg/incidents/incstore/store.go +++ b/pkg/incidents/incstore/store.go @@ -2,9 +2,12 @@ package incstore import ( "context" + "time" "dynatron.me/x/stillbox/internal/common" "dynatron.me/x/stillbox/internal/jsontypes" + "dynatron.me/x/stillbox/pkg/auth" + "dynatron.me/x/stillbox/pkg/calls" "dynatron.me/x/stillbox/pkg/database" "dynatron.me/x/stillbox/pkg/incidents" "github.com/google/uuid" @@ -25,13 +28,16 @@ type Store interface { // AddToIncident adds the specified call IDs to an incident. // If not nil, notes must be valid json. - AddToIncident(ctx context.Context, incidentID uuid.UUID, callIDs []uuid.UUID, notes []byte) error + AddRemoveIncidentCalls(ctx context.Context, incidentID uuid.UUID, addCallIDs []uuid.UUID, notes []byte, removeCallIDs []uuid.UUID) error + + // UpdateNotes updates the notes for a call-incident mapping. + UpdateNotes(ctx context.Context, incidentID uuid.UUID, callID uuid.UUID, notes []byte) error // Incidents gets incidents matching parameters and pagination. Incidents(ctx context.Context, p IncidentsParams) (incs []database.Incident, totalCount int, err error) // Incident gets a single incident. - Incident(ctx context.Context, id uuid.UUID) (database.Incident, error) + Incident(ctx context.Context, id uuid.UUID) (*incidents.Incident, error) // UpdateIncident updates an incident. UpdateIncident(ctx context.Context, id uuid.UUID, p UpdateIncidentParams) (database.Incident, error) @@ -79,18 +85,32 @@ func (s *store) CreateIncident(ctx context.Context, inc incidents.Incident) (dat return dbInc, err } -func (s *store) AddToIncident(ctx context.Context, incidentID uuid.UUID, callIDs []uuid.UUID, notes []byte) error { - db := database.FromCtx(ctx) +func (s *store) AddRemoveIncidentCalls(ctx context.Context, incidentID uuid.UUID, addCallIDs []uuid.UUID, notes []byte, removeCallIDs []uuid.UUID) error { + return database.FromCtx(ctx).InTx(ctx, func(db database.Store) error { + if len(addCallIDs) > 0 { + var noteAr [][]byte + if notes != nil { + noteAr = make([][]byte, len(addCallIDs)) + for i := range addCallIDs { + noteAr[i] = notes + } + } - var noteAr [][]byte - if notes != nil { - noteAr = make([][]byte, len(callIDs)) - for i := range callIDs { - noteAr[i] = notes + err := db.AddToIncident(ctx, incidentID, addCallIDs, noteAr) + if err != nil { + return err + } } - } - return db.AddToIncident(ctx, incidentID, callIDs, noteAr) + if len(removeCallIDs) > 0 { + err := db.RemoveFromIncident(ctx, incidentID, removeCallIDs) + if err != nil { + return err + } + } + + return nil + }, pgx.TxOptions{}) } func (s *store) Incidents(ctx context.Context, p IncidentsParams) (rows []database.Incident, totalCount int, err error) { @@ -123,8 +143,67 @@ func (s *store) Incidents(ctx context.Context, p IncidentsParams) (rows []databa return rows, int(count), err } -func (s *store) Incident(ctx context.Context, id uuid.UUID) (database.Incident, error) { - return database.FromCtx(ctx).GetIncident(ctx, id) +func fromDBIncident(id uuid.UUID, d database.Incident) incidents.Incident { + return incidents.Incident{ + ID: id, + Name: d.Name, + Description: d.Description, + StartTime: jsontypes.TimePtrFromTSTZ(d.StartTime), + EndTime: jsontypes.TimePtrFromTSTZ(d.EndTime), + Metadata: d.Metadata, + } +} + +func fromDBCalls(d []database.GetIncidentCallsRow) []incidents.IncidentCall { + r := make([]incidents.IncidentCall, 0, len(d)) + for _, v := range d { + dur := calls.CallDuration(time.Duration(common.ZeroIfNil(v.Duration)) * time.Millisecond) + sub := common.PtrTo(auth.UserID(common.ZeroIfNil(v.Submitter))) + r = append(r, incidents.IncidentCall{ + Call: calls.Call{ + ID: v.CallID, + AudioName: common.ZeroIfNil(v.AudioName), + AudioType: common.ZeroIfNil(v.AudioType), + Duration: dur, + DateTime: v.CallDate.Time, + Frequencies: v.Frequencies, + Frequency: v.Frequency, + Patches: v.Patches, + Source: v.Source, + System: v.System, + Submitter: sub, + Talkgroup: v.Talkgroup, + }, + Notes: v.Notes, + }) + } + + return r +} + +func (s *store) Incident(ctx context.Context, id uuid.UUID) (*incidents.Incident, error) { + var r incidents.Incident + txErr := database.FromCtx(ctx).InTx(ctx, func(db database.Store) error { + inc, err := db.GetIncident(ctx, id) + if err != nil { + return err + } + + calls, err := db.GetIncidentCalls(ctx, id) + if err != nil { + return err + } + + r = fromDBIncident(id, inc) + r.Calls = fromDBCalls(calls) + + return nil + }, pgx.TxOptions{}) + if txErr != nil { + return nil, txErr + } + + return &r, nil } type UpdateIncidentParams struct { @@ -157,3 +236,7 @@ func (s *store) UpdateIncident(ctx context.Context, id uuid.UUID, p UpdateIncide func (s *store) DeleteIncident(ctx context.Context, id uuid.UUID) error { return database.FromCtx(ctx).DeleteIncident(ctx, id) } + +func (s *store) UpdateNotes(ctx context.Context, incidentID uuid.UUID, callID uuid.UUID, notes []byte) error { + return database.FromCtx(ctx).UpdateCallIncidentNotes(ctx, notes, incidentID, callID) +} diff --git a/pkg/pb/stillbox.pb.go b/pkg/pb/stillbox.pb.go index 8d29825..f473933 100644 --- a/pkg/pb/stillbox.pb.go +++ b/pkg/pb/stillbox.pb.go @@ -298,9 +298,8 @@ type Call struct { Frequency int64 `protobuf:"varint,8,opt,name=frequency,proto3" json:"frequency,omitempty"` Frequencies []int64 `protobuf:"varint,9,rep,packed,name=frequencies,proto3" json:"frequencies,omitempty"` Patches []int32 `protobuf:"varint,10,rep,packed,name=patches,proto3" json:"patches,omitempty"` - Sources []int32 `protobuf:"varint,11,rep,packed,name=sources,proto3" json:"sources,omitempty"` - Duration *int32 `protobuf:"varint,12,opt,name=duration,proto3,oneof" json:"duration,omitempty"` - Audio []byte `protobuf:"bytes,13,opt,name=audio,proto3" json:"audio,omitempty"` + Duration *int32 `protobuf:"varint,11,opt,name=duration,proto3,oneof" json:"duration,omitempty"` + Audio []byte `protobuf:"bytes,12,opt,name=audio,proto3" json:"audio,omitempty"` } func (x *Call) Reset() { @@ -405,13 +404,6 @@ func (x *Call) GetPatches() []int32 { return nil } -func (x *Call) GetSources() []int32 { - if x != nil { - return x.Sources - } - return nil -} - func (x *Call) GetDuration() int32 { if x != nil && x.Duration != nil { return *x.Duration @@ -1195,7 +1187,7 @@ var file_stillbox_proto_rawDesc = []byte{ 0x6f, 0x75, 0x70, 0x49, 0x6e, 0x66, 0x6f, 0x48, 0x00, 0x52, 0x06, 0x74, 0x67, 0x49, 0x6e, 0x66, 0x6f, 0x42, 0x12, 0x0a, 0x10, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x5f, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x0d, 0x0a, 0x0b, 0x5f, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, - 0x64, 0x5f, 0x69, 0x64, 0x22, 0x91, 0x03, 0x0a, 0x04, 0x43, 0x61, 0x6c, 0x6c, 0x12, 0x0e, 0x0a, + 0x64, 0x5f, 0x69, 0x64, 0x22, 0xf7, 0x02, 0x0a, 0x04, 0x43, 0x61, 0x6c, 0x6c, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x61, 0x75, 0x64, 0x69, 0x6f, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x61, 0x75, 0x64, 0x69, 0x6f, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x61, @@ -1214,112 +1206,111 @@ var file_stillbox_proto_rawDesc = []byte{ 0x0a, 0x0b, 0x66, 0x72, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x69, 0x65, 0x73, 0x18, 0x09, 0x20, 0x03, 0x28, 0x03, 0x52, 0x0b, 0x66, 0x72, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x69, 0x65, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x61, 0x74, 0x63, 0x68, 0x65, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, - 0x05, 0x52, 0x07, 0x70, 0x61, 0x74, 0x63, 0x68, 0x65, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x0b, 0x20, 0x03, 0x28, 0x05, 0x52, 0x07, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x73, 0x12, 0x1f, 0x0a, 0x08, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x18, 0x0c, 0x20, 0x01, 0x28, 0x05, 0x48, 0x00, 0x52, 0x08, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x88, 0x01, 0x01, 0x12, 0x14, 0x0a, 0x05, 0x61, 0x75, 0x64, 0x69, 0x6f, 0x18, 0x0d, - 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x61, 0x75, 0x64, 0x69, 0x6f, 0x42, 0x0b, 0x0a, 0x09, 0x5f, - 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x3e, 0x0a, 0x05, 0x48, 0x65, 0x6c, 0x6c, - 0x6f, 0x12, 0x35, 0x0a, 0x0b, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, 0x69, 0x6e, 0x66, 0x6f, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x73, 0x74, 0x69, 0x6c, 0x6c, 0x62, 0x6f, - 0x78, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x0a, 0x73, 0x65, - 0x72, 0x76, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x22, 0x1d, 0x0a, 0x09, 0x55, 0x73, 0x65, 0x72, - 0x50, 0x6f, 0x70, 0x75, 0x70, 0x12, 0x10, 0x0a, 0x03, 0x6d, 0x73, 0x67, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x03, 0x6d, 0x73, 0x67, 0x22, 0x4a, 0x0a, 0x05, 0x45, 0x72, 0x72, 0x6f, 0x72, - 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x2b, 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, - 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x73, 0x74, 0x69, 0x6c, 0x6c, 0x62, - 0x6f, 0x78, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x52, 0x07, 0x63, 0x6f, 0x6d, 0x6d, - 0x61, 0x6e, 0x64, 0x22, 0x78, 0x0a, 0x0c, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x12, 0x37, 0x0a, 0x09, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, - 0x6d, 0x70, 0x52, 0x08, 0x64, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, - 0x6d, 0x73, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6d, 0x73, 0x67, 0x12, 0x1d, - 0x0a, 0x0a, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x09, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x55, 0x72, 0x6c, 0x22, 0xed, 0x01, - 0x0a, 0x07, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x22, 0x0a, 0x0a, 0x63, 0x6f, 0x6d, - 0x6d, 0x61, 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x48, 0x01, 0x52, - 0x09, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x49, 0x64, 0x88, 0x01, 0x01, 0x12, 0x33, 0x0a, - 0x0c, 0x6c, 0x69, 0x76, 0x65, 0x5f, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x73, 0x74, 0x69, 0x6c, 0x6c, 0x62, 0x6f, 0x78, 0x2e, 0x4c, - 0x69, 0x76, 0x65, 0x48, 0x00, 0x52, 0x0b, 0x6c, 0x69, 0x76, 0x65, 0x43, 0x6f, 0x6d, 0x6d, 0x61, - 0x6e, 0x64, 0x12, 0x39, 0x0a, 0x0e, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x5f, 0x63, 0x6f, 0x6d, - 0x6d, 0x61, 0x6e, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x73, 0x74, 0x69, - 0x6c, 0x6c, 0x62, 0x6f, 0x78, 0x2e, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x48, 0x00, 0x52, 0x0d, - 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x34, 0x0a, - 0x0a, 0x74, 0x67, 0x5f, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x05, 0x52, 0x07, 0x70, 0x61, 0x74, 0x63, 0x68, 0x65, 0x73, 0x12, 0x1f, 0x0a, 0x08, 0x64, 0x75, + 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x05, 0x48, 0x00, 0x52, 0x08, + 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x88, 0x01, 0x01, 0x12, 0x14, 0x0a, 0x05, 0x61, + 0x75, 0x64, 0x69, 0x6f, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x61, 0x75, 0x64, 0x69, + 0x6f, 0x42, 0x0b, 0x0a, 0x09, 0x5f, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x3e, + 0x0a, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x12, 0x35, 0x0a, 0x0b, 0x73, 0x65, 0x72, 0x76, 0x65, + 0x72, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x73, + 0x74, 0x69, 0x6c, 0x6c, 0x62, 0x6f, 0x78, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x49, 0x6e, + 0x66, 0x6f, 0x52, 0x0a, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x22, 0x1d, + 0x0a, 0x09, 0x55, 0x73, 0x65, 0x72, 0x50, 0x6f, 0x70, 0x75, 0x70, 0x12, 0x10, 0x0a, 0x03, 0x6d, + 0x73, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6d, 0x73, 0x67, 0x22, 0x4a, 0x0a, + 0x05, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x2b, 0x0a, 0x07, + 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, + 0x73, 0x74, 0x69, 0x6c, 0x6c, 0x62, 0x6f, 0x78, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, + 0x52, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x22, 0x78, 0x0a, 0x0c, 0x4e, 0x6f, 0x74, + 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x37, 0x0a, 0x09, 0x64, 0x61, 0x74, + 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, + 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, + 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x08, 0x64, 0x61, 0x74, 0x65, 0x54, 0x69, + 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x6d, 0x73, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x03, 0x6d, 0x73, 0x67, 0x12, 0x1d, 0x0a, 0x0a, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x75, + 0x72, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x55, 0x72, 0x6c, 0x22, 0xed, 0x01, 0x0a, 0x07, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12, + 0x22, 0x0a, 0x0a, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x03, 0x48, 0x01, 0x52, 0x09, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x49, 0x64, + 0x88, 0x01, 0x01, 0x12, 0x33, 0x0a, 0x0c, 0x6c, 0x69, 0x76, 0x65, 0x5f, 0x63, 0x6f, 0x6d, 0x6d, + 0x61, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x73, 0x74, 0x69, 0x6c, + 0x6c, 0x62, 0x6f, 0x78, 0x2e, 0x4c, 0x69, 0x76, 0x65, 0x48, 0x00, 0x52, 0x0b, 0x6c, 0x69, 0x76, + 0x65, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x39, 0x0a, 0x0e, 0x73, 0x65, 0x61, 0x72, + 0x63, 0x68, 0x5f, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x10, 0x2e, 0x73, 0x74, 0x69, 0x6c, 0x6c, 0x62, 0x6f, 0x78, 0x2e, 0x53, 0x65, 0x61, 0x72, + 0x63, 0x68, 0x48, 0x00, 0x52, 0x0d, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x43, 0x6f, 0x6d, 0x6d, + 0x61, 0x6e, 0x64, 0x12, 0x34, 0x0a, 0x0a, 0x74, 0x67, 0x5f, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, + 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x73, 0x74, 0x69, 0x6c, 0x6c, 0x62, + 0x6f, 0x78, 0x2e, 0x54, 0x61, 0x6c, 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x48, 0x00, 0x52, 0x09, + 0x74, 0x67, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x42, 0x09, 0x0a, 0x07, 0x63, 0x6f, 0x6d, + 0x6d, 0x61, 0x6e, 0x64, 0x42, 0x0d, 0x0a, 0x0b, 0x5f, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, + 0x5f, 0x69, 0x64, 0x22, 0xf2, 0x02, 0x0a, 0x0d, 0x54, 0x61, 0x6c, 0x6b, 0x67, 0x72, 0x6f, 0x75, + 0x70, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x23, 0x0a, 0x02, 0x74, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x73, 0x74, 0x69, 0x6c, 0x6c, 0x62, 0x6f, 0x78, 0x2e, 0x54, 0x61, 0x6c, - 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x48, 0x00, 0x52, 0x09, 0x74, 0x67, 0x43, 0x6f, 0x6d, 0x6d, - 0x61, 0x6e, 0x64, 0x42, 0x09, 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x42, 0x0d, - 0x0a, 0x0b, 0x5f, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x22, 0xf2, 0x02, - 0x0a, 0x0d, 0x54, 0x61, 0x6c, 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x49, 0x6e, 0x66, 0x6f, 0x12, - 0x23, 0x0a, 0x02, 0x74, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x73, 0x74, - 0x69, 0x6c, 0x6c, 0x62, 0x6f, 0x78, 0x2e, 0x54, 0x61, 0x6c, 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x70, - 0x52, 0x02, 0x74, 0x67, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x5f, 0x6e, - 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x79, 0x73, 0x74, 0x65, - 0x6d, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x17, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x88, 0x01, 0x01, 0x12, 0x19, - 0x0a, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x48, 0x01, 0x52, - 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x88, 0x01, 0x01, 0x12, 0x20, 0x0a, 0x09, 0x61, 0x6c, 0x70, - 0x68, 0x61, 0x5f, 0x74, 0x61, 0x67, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x48, 0x02, 0x52, 0x08, - 0x61, 0x6c, 0x70, 0x68, 0x61, 0x54, 0x61, 0x67, 0x88, 0x01, 0x01, 0x12, 0x21, 0x0a, 0x09, 0x66, - 0x72, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x79, 0x18, 0x06, 0x20, 0x01, 0x28, 0x05, 0x48, 0x03, - 0x52, 0x09, 0x66, 0x72, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x79, 0x88, 0x01, 0x01, 0x12, 0x12, - 0x0a, 0x04, 0x74, 0x61, 0x67, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x09, 0x52, 0x04, 0x74, 0x61, - 0x67, 0x73, 0x12, 0x38, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x08, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x48, 0x04, 0x52, - 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x88, 0x01, 0x01, 0x12, 0x18, 0x0a, 0x07, - 0x6c, 0x65, 0x61, 0x72, 0x6e, 0x65, 0x64, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x6c, - 0x65, 0x61, 0x72, 0x6e, 0x65, 0x64, 0x42, 0x07, 0x0a, 0x05, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x42, - 0x08, 0x0a, 0x06, 0x5f, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x61, 0x6c, - 0x70, 0x68, 0x61, 0x5f, 0x74, 0x61, 0x67, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x66, 0x72, 0x65, 0x71, - 0x75, 0x65, 0x6e, 0x63, 0x79, 0x42, 0x0b, 0x0a, 0x09, 0x5f, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, - 0x74, 0x61, 0x22, 0x7a, 0x0a, 0x04, 0x4c, 0x69, 0x76, 0x65, 0x12, 0x2e, 0x0a, 0x05, 0x73, 0x74, - 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x13, 0x2e, 0x73, 0x74, 0x69, 0x6c, - 0x6c, 0x62, 0x6f, 0x78, 0x2e, 0x4c, 0x69, 0x76, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x48, 0x00, - 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x88, 0x01, 0x01, 0x12, 0x2d, 0x0a, 0x06, 0x66, 0x69, - 0x6c, 0x74, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x73, 0x74, 0x69, - 0x6c, 0x6c, 0x62, 0x6f, 0x78, 0x2e, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x48, 0x01, 0x52, 0x06, - 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x88, 0x01, 0x01, 0x42, 0x08, 0x0a, 0x06, 0x5f, 0x73, 0x74, - 0x61, 0x74, 0x65, 0x42, 0x09, 0x0a, 0x07, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x22, 0x41, - 0x0a, 0x09, 0x54, 0x61, 0x6c, 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x16, 0x0a, 0x06, 0x73, - 0x79, 0x73, 0x74, 0x65, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x73, 0x79, 0x73, - 0x74, 0x65, 0x6d, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x61, 0x6c, 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x70, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x74, 0x61, 0x6c, 0x6b, 0x67, 0x72, 0x6f, 0x75, - 0x70, 0x22, 0x83, 0x02, 0x0a, 0x06, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x12, 0x33, 0x0a, 0x0a, - 0x74, 0x61, 0x6c, 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x13, 0x2e, 0x73, 0x74, 0x69, 0x6c, 0x6c, 0x62, 0x6f, 0x78, 0x2e, 0x54, 0x61, 0x6c, 0x6b, - 0x67, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x0a, 0x74, 0x61, 0x6c, 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x70, - 0x73, 0x12, 0x3a, 0x0a, 0x0e, 0x74, 0x61, 0x6c, 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x5f, - 0x6e, 0x6f, 0x74, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x73, 0x74, 0x69, 0x6c, - 0x6c, 0x62, 0x6f, 0x78, 0x2e, 0x54, 0x61, 0x6c, 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x0d, - 0x74, 0x61, 0x6c, 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x4e, 0x6f, 0x74, 0x12, 0x2c, 0x0a, - 0x12, 0x74, 0x61, 0x6c, 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x74, 0x61, 0x67, 0x73, 0x5f, - 0x61, 0x6c, 0x6c, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x10, 0x74, 0x61, 0x6c, 0x6b, 0x67, - 0x72, 0x6f, 0x75, 0x70, 0x54, 0x61, 0x67, 0x73, 0x41, 0x6c, 0x6c, 0x12, 0x2c, 0x0a, 0x12, 0x74, - 0x61, 0x6c, 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x74, 0x61, 0x67, 0x73, 0x5f, 0x61, 0x6e, - 0x79, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x10, 0x74, 0x61, 0x6c, 0x6b, 0x67, 0x72, 0x6f, - 0x75, 0x70, 0x54, 0x61, 0x67, 0x73, 0x41, 0x6e, 0x79, 0x12, 0x2c, 0x0a, 0x12, 0x74, 0x61, 0x6c, - 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x74, 0x61, 0x67, 0x73, 0x5f, 0x6e, 0x6f, 0x74, 0x18, - 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x10, 0x74, 0x61, 0x6c, 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x70, - 0x54, 0x61, 0x67, 0x73, 0x4e, 0x6f, 0x74, 0x22, 0x08, 0x0a, 0x06, 0x53, 0x65, 0x61, 0x72, 0x63, - 0x68, 0x22, 0x92, 0x01, 0x0a, 0x0a, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, - 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4e, 0x61, 0x6d, - 0x65, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x62, - 0x75, 0x69, 0x6c, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x62, 0x75, 0x69, 0x6c, - 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x18, 0x04, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x12, 0x17, 0x0a, - 0x07, 0x64, 0x62, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, - 0x64, 0x62, 0x53, 0x69, 0x7a, 0x65, 0x2a, 0x37, 0x0a, 0x09, 0x4c, 0x69, 0x76, 0x65, 0x53, 0x74, - 0x61, 0x74, 0x65, 0x12, 0x0e, 0x0a, 0x0a, 0x4c, 0x53, 0x5f, 0x53, 0x54, 0x4f, 0x50, 0x50, 0x45, - 0x44, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x4c, 0x53, 0x5f, 0x4c, 0x49, 0x56, 0x45, 0x10, 0x01, - 0x12, 0x0d, 0x0a, 0x09, 0x4c, 0x53, 0x5f, 0x50, 0x41, 0x55, 0x53, 0x45, 0x44, 0x10, 0x02, 0x42, - 0x06, 0x5a, 0x04, 0x2e, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x02, 0x74, 0x67, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x79, + 0x73, 0x74, 0x65, 0x6d, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0a, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x17, 0x0a, 0x04, 0x6e, + 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x04, 0x6e, 0x61, 0x6d, + 0x65, 0x88, 0x01, 0x01, 0x12, 0x19, 0x0a, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x18, 0x04, 0x20, + 0x01, 0x28, 0x09, 0x48, 0x01, 0x52, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x88, 0x01, 0x01, 0x12, + 0x20, 0x0a, 0x09, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x5f, 0x74, 0x61, 0x67, 0x18, 0x05, 0x20, 0x01, + 0x28, 0x09, 0x48, 0x02, 0x52, 0x08, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x54, 0x61, 0x67, 0x88, 0x01, + 0x01, 0x12, 0x21, 0x0a, 0x09, 0x66, 0x72, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x79, 0x18, 0x06, + 0x20, 0x01, 0x28, 0x05, 0x48, 0x03, 0x52, 0x09, 0x66, 0x72, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, + 0x79, 0x88, 0x01, 0x01, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x61, 0x67, 0x73, 0x18, 0x07, 0x20, 0x03, + 0x28, 0x09, 0x52, 0x04, 0x74, 0x61, 0x67, 0x73, 0x12, 0x38, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, + 0x64, 0x61, 0x74, 0x61, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, + 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, + 0x75, 0x63, 0x74, 0x48, 0x04, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x88, + 0x01, 0x01, 0x12, 0x18, 0x0a, 0x07, 0x6c, 0x65, 0x61, 0x72, 0x6e, 0x65, 0x64, 0x18, 0x09, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x07, 0x6c, 0x65, 0x61, 0x72, 0x6e, 0x65, 0x64, 0x42, 0x07, 0x0a, 0x05, + 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x42, 0x08, 0x0a, 0x06, 0x5f, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x42, + 0x0c, 0x0a, 0x0a, 0x5f, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x5f, 0x74, 0x61, 0x67, 0x42, 0x0c, 0x0a, + 0x0a, 0x5f, 0x66, 0x72, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x79, 0x42, 0x0b, 0x0a, 0x09, 0x5f, + 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0x7a, 0x0a, 0x04, 0x4c, 0x69, 0x76, 0x65, + 0x12, 0x2e, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, + 0x13, 0x2e, 0x73, 0x74, 0x69, 0x6c, 0x6c, 0x62, 0x6f, 0x78, 0x2e, 0x4c, 0x69, 0x76, 0x65, 0x53, + 0x74, 0x61, 0x74, 0x65, 0x48, 0x00, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x88, 0x01, 0x01, + 0x12, 0x2d, 0x0a, 0x06, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x10, 0x2e, 0x73, 0x74, 0x69, 0x6c, 0x6c, 0x62, 0x6f, 0x78, 0x2e, 0x46, 0x69, 0x6c, 0x74, + 0x65, 0x72, 0x48, 0x01, 0x52, 0x06, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x88, 0x01, 0x01, 0x42, + 0x08, 0x0a, 0x06, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x42, 0x09, 0x0a, 0x07, 0x5f, 0x66, 0x69, + 0x6c, 0x74, 0x65, 0x72, 0x22, 0x41, 0x0a, 0x09, 0x54, 0x61, 0x6c, 0x6b, 0x67, 0x72, 0x6f, 0x75, + 0x70, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x05, 0x52, 0x06, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x61, 0x6c, + 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x74, 0x61, + 0x6c, 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x22, 0x83, 0x02, 0x0a, 0x06, 0x46, 0x69, 0x6c, 0x74, + 0x65, 0x72, 0x12, 0x33, 0x0a, 0x0a, 0x74, 0x61, 0x6c, 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, + 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x73, 0x74, 0x69, 0x6c, 0x6c, 0x62, 0x6f, + 0x78, 0x2e, 0x54, 0x61, 0x6c, 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x0a, 0x74, 0x61, 0x6c, + 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x12, 0x3a, 0x0a, 0x0e, 0x74, 0x61, 0x6c, 0x6b, 0x67, + 0x72, 0x6f, 0x75, 0x70, 0x73, 0x5f, 0x6e, 0x6f, 0x74, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x13, 0x2e, 0x73, 0x74, 0x69, 0x6c, 0x6c, 0x62, 0x6f, 0x78, 0x2e, 0x54, 0x61, 0x6c, 0x6b, 0x67, + 0x72, 0x6f, 0x75, 0x70, 0x52, 0x0d, 0x74, 0x61, 0x6c, 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, + 0x4e, 0x6f, 0x74, 0x12, 0x2c, 0x0a, 0x12, 0x74, 0x61, 0x6c, 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x70, + 0x5f, 0x74, 0x61, 0x67, 0x73, 0x5f, 0x61, 0x6c, 0x6c, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, + 0x10, 0x74, 0x61, 0x6c, 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x54, 0x61, 0x67, 0x73, 0x41, 0x6c, + 0x6c, 0x12, 0x2c, 0x0a, 0x12, 0x74, 0x61, 0x6c, 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x74, + 0x61, 0x67, 0x73, 0x5f, 0x61, 0x6e, 0x79, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x10, 0x74, + 0x61, 0x6c, 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x54, 0x61, 0x67, 0x73, 0x41, 0x6e, 0x79, 0x12, + 0x2c, 0x0a, 0x12, 0x74, 0x61, 0x6c, 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x74, 0x61, 0x67, + 0x73, 0x5f, 0x6e, 0x6f, 0x74, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x10, 0x74, 0x61, 0x6c, + 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x54, 0x61, 0x67, 0x73, 0x4e, 0x6f, 0x74, 0x22, 0x08, 0x0a, + 0x06, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x22, 0x92, 0x01, 0x0a, 0x0a, 0x53, 0x65, 0x72, 0x76, + 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, + 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x65, 0x72, + 0x76, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, + 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, + 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x62, 0x75, 0x69, 0x6c, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x05, 0x62, 0x75, 0x69, 0x6c, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x6c, 0x61, 0x74, 0x66, + 0x6f, 0x72, 0x6d, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x6c, 0x61, 0x74, 0x66, + 0x6f, 0x72, 0x6d, 0x12, 0x17, 0x0a, 0x07, 0x64, 0x62, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x05, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x64, 0x62, 0x53, 0x69, 0x7a, 0x65, 0x2a, 0x37, 0x0a, 0x09, + 0x4c, 0x69, 0x76, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x0e, 0x0a, 0x0a, 0x4c, 0x53, 0x5f, + 0x53, 0x54, 0x4f, 0x50, 0x50, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x4c, 0x53, 0x5f, + 0x4c, 0x49, 0x56, 0x45, 0x10, 0x01, 0x12, 0x0d, 0x0a, 0x09, 0x4c, 0x53, 0x5f, 0x50, 0x41, 0x55, + 0x53, 0x45, 0x44, 0x10, 0x02, 0x42, 0x06, 0x5a, 0x04, 0x2e, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/pkg/pb/stillbox.proto b/pkg/pb/stillbox.proto index 815c9b3..01751bd 100644 --- a/pkg/pb/stillbox.proto +++ b/pkg/pb/stillbox.proto @@ -34,9 +34,8 @@ message Call { int64 frequency = 8; repeated int64 frequencies = 9; repeated int32 patches = 10; - repeated int32 sources = 11; - optional int32 duration = 12; - bytes audio = 13; + optional int32 duration = 11; + bytes audio = 12; } message Hello { diff --git a/pkg/rest/api.go b/pkg/rest/api.go index 11e5e05..6998f9d 100644 --- a/pkg/rest/api.go +++ b/pkg/rest/api.go @@ -9,6 +9,7 @@ import ( "github.com/go-chi/chi/v5" "github.com/go-chi/render" "github.com/go-viper/mapstructure/v2" + "github.com/google/uuid" "github.com/jackc/pgx/v5" "github.com/rs/zerolog/log" ) @@ -174,6 +175,21 @@ func decodeParams(d interface{}, r *http.Request) error { return dec.Decode(m) } +// idOnlyParam checks for a sole URL parameter, id, and writes an errorif this fails. +func idOnlyParam(w http.ResponseWriter, r *http.Request) (uuid.UUID, error) { + params := struct { + ID uuid.UUID `param:"id"` + }{} + + err := decodeParams(¶ms, r) + if err != nil { + wErr(w, r, badRequest(err)) + return uuid.UUID{}, err + } + + return params.ID, nil +} + func respond(w http.ResponseWriter, r *http.Request, v interface{}) { render.DefaultResponder(w, r, v) } diff --git a/pkg/rest/incidents.go b/pkg/rest/incidents.go index 564d282..9e09f1b 100644 --- a/pkg/rest/incidents.go +++ b/pkg/rest/incidents.go @@ -1,12 +1,16 @@ package rest import ( + "bytes" + "encoding/json" + "fmt" "net/http" "dynatron.me/x/stillbox/internal/forms" "dynatron.me/x/stillbox/pkg/database" "dynatron.me/x/stillbox/pkg/incidents" "dynatron.me/x/stillbox/pkg/incidents/incstore" + "dynatron.me/x/stillbox/pkg/talkgroups/tgstore" "github.com/go-chi/chi/v5" "github.com/google/uuid" @@ -19,9 +23,11 @@ func (ia *incidentsAPI) Subrouter() http.Handler { r := chi.NewMux() r.Get(`/{id:[a-f0-9-]+}`, ia.getIncident) + r.Get(`/{id:[a-f0-9-]+}.m3u`, ia.getCallsM3U) r.Post(`/create`, ia.createIncident) r.Post(`/`, ia.listIncidents) + r.Post(`/{id:[a-f0-9-]+}/calls`, ia.postCalls) r.Put(`/{id:[a-f0-9]+}`, ia.updateIncident) @@ -79,17 +85,12 @@ func (ia *incidentsAPI) getIncident(w http.ResponseWriter, r *http.Request) { ctx := r.Context() incs := incstore.FromCtx(ctx) - params := struct { - ID uuid.UUID `param:"id"` - }{} - - err := decodeParams(¶ms, r) + id, err := idOnlyParam(w, r) if err != nil { - wErr(w, r, badRequest(err)) return } - inc, err := incs.Incident(ctx, params.ID) + inc, err := incs.Incident(ctx, id) if err != nil { wErr(w, r, autoError(err)) return @@ -102,13 +103,8 @@ func (ia *incidentsAPI) updateIncident(w http.ResponseWriter, r *http.Request) { ctx := r.Context() incs := incstore.FromCtx(ctx) - urlParams := struct { - ID uuid.UUID `param:"id"` - }{} - - err := decodeParams(&urlParams, r) + id, err := idOnlyParam(w, r) if err != nil { - wErr(w, r, badRequest(err)) return } @@ -119,7 +115,7 @@ func (ia *incidentsAPI) updateIncident(w http.ResponseWriter, r *http.Request) { return } - inc, err := incs.UpdateIncident(ctx, urlParams.ID, p) + inc, err := incs.UpdateIncident(ctx, id, p) if err != nil { wErr(w, r, autoError(err)) return @@ -150,3 +146,87 @@ func (ia *incidentsAPI) deleteIncident(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNoContent) } + +type CallIncidentParams struct { + Add []uuid.UUID `json:"add"` + Notes json.RawMessage `json:"notes"` + + Remove []uuid.UUID `json:"remove"` +} + +func (ia *incidentsAPI) postCalls(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + incs := incstore.FromCtx(ctx) + + id, err := idOnlyParam(w, r) + if err != nil { + return + } + + p := CallIncidentParams{} + err = forms.Unmarshal(r, &p, forms.WithTag("json"), forms.WithAcceptBlank(), forms.WithOmitEmpty()) + if err != nil { + wErr(w, r, badRequest(err)) + return + } + + err = incs.AddRemoveIncidentCalls(ctx, id, p.Add, p.Notes, p.Remove) + if err != nil { + wErr(w, r, autoError(err)) + return + } + + w.WriteHeader(http.StatusNoContent) +} + +func (ia *incidentsAPI) getCallsM3U(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + incs := incstore.FromCtx(ctx) + tgst := tgstore.FromCtx(ctx) + + id, err := idOnlyParam(w, r) + if err != nil { + return + } + + inc, err := incs.Incident(ctx, id) + if err != nil { + wErr(w, r, autoError(err)) + return + } + + var b bytes.Buffer + + callUrl := r.URL + callUrl.RawQuery = "" + callUrl.Fragment = "" + + b.WriteString("#EXTM3U\n\n") + for _, c := range inc.Calls { + tg, err := tgst.TG(ctx, c.TalkgroupTuple()) + if err != nil { + wErr(w, r, autoError(err)) + return + } + var from string + if c.Source != 0 { + from = fmt.Sprintf(" from %d", c.Source) + } + + callUrl.Path = "/api/call/%s" + c.ID.String() + + fmt.Fprintf(w, "#EXTINF:%d,%s%s (%s)\n%s\n\n", + c.Duration.Seconds(), + tg.StringTag(true), + from, + c.DateTime.Format("15:04 01/02"), + callUrl, + ) + } + + // Not a lot of agreement on which MIME type to use for non-HLS m3u, + // let's hope this is good enough + w.Header().Set("Content-Type", "audio/x-mpegurl") + w.WriteHeader(http.StatusOK) + _, _ = b.WriteTo(w) +} diff --git a/pkg/sinks/relay.go b/pkg/sinks/relay.go index a481764..1c75db1 100644 --- a/pkg/sinks/relay.go +++ b/pkg/sinks/relay.go @@ -81,7 +81,7 @@ func (s *Relay) Call(ctx context.Context, call *calls.Call) error { var buf bytes.Buffer body := multipart.NewWriter(&buf) - err := forms.Marshal(call, body) + err := forms.Marshal(call, body, forms.WithTag("json")) if err != nil { return fmt.Errorf("relay form parse: %w", err) } diff --git a/pkg/sources/http.go b/pkg/sources/http.go index d9fe4e0..1c18b39 100644 --- a/pkg/sources/http.go +++ b/pkg/sources/http.go @@ -46,7 +46,6 @@ type CallUploadRequest struct { Key string `form:"key"` Patches []int `form:"patches"` Source int `form:"source"` - Sources []int `form:"sources"` System int `form:"system"` SystemLabel string `form:"systemLabel"` Talkgroup int `form:"talkgroup"` diff --git a/sql/postgres/queries/calls.sql b/sql/postgres/queries/calls.sql index 139d0ff..25a2d34 100644 --- a/sql/postgres/queries/calls.sql +++ b/sql/postgres/queries/calls.sql @@ -38,7 +38,22 @@ source ); -- name: GetCallAudioByID :one -SELECT call_date, audio_name, audio_type, audio_blob FROM calls WHERE id = @id; +SELECT + c.call_date, + c.audio_name, + c.audio_type, + c.audio_blob +FROM calls c +WHERE c.id = @id +UNION +SELECT + sc.call_date, + sc.audio_name, + sc.audio_type, + sc.audio_blob +FROM swept_calls sc +WHERE sc.id = @id +; -- name: SetCallTranscript :exec UPDATE calls SET transcript = $2 WHERE id = $1; diff --git a/sql/postgres/queries/incidents.sql b/sql/postgres/queries/incidents.sql index ee7ed74..502c762 100644 --- a/sql/postgres/queries/incidents.sql +++ b/sql/postgres/queries/incidents.sql @@ -20,6 +20,15 @@ FROM inp JOIN calls c ON c.id = inp.id ; +-- name: RemoveFromIncident :exec +DELETE FROM incidents_calls ic +WHERE ic.incident_id = @id AND ic.call_id = ANY(@call_ids::UUID[]); + +-- name: UpdateCallIncidentNotes :exec +UPDATE incidents_Calls +SET notes = @notes +WHERE incident_id = @incident_id AND call_id = @call_id; + -- name: CreateIncident :one INSERT INTO incidents ( id, @@ -73,13 +82,40 @@ CASE WHEN sqlc.narg('end')::TIMESTAMPTZ IS NOT NULL THEN i.start_time <= sqlc.narg('end') ELSE TRUE END ; --- name: IncidentCalls :many --- INCOMPLETE -SELECT -ic.incident_id, call_date, -ic.call_id, -ic.notes -FROM incidents_calls ic; +-- name: GetIncidentCalls :many +SELECT ic.call_id, ic.call_date, ic.notes, c.* +FROM incidents_calls ic, LATERAL ( + SELECT + ca.submitter, + ca.system, + ca.talkgroup, + ca.audio_name, + ca.duration, + ca.audio_type, + ca.audio_url, + ca.frequency, + ca.frequencies, + ca.patches, + ca.source, + ca.transcript + FROM calls ca WHERE ca.id = ic.calls_tbl_id AND ca.call_date = ic.call_date + UNION + SELECT + sc.submitter, + sc.system, + sc.talkgroup, + sc.audio_name, + sc.duration, + sc.audio_type, + sc.audio_url, + sc.frequency, + sc.frequencies, + sc.patches, + sc.source, + sc.transcript + FROM swept_calls sc WHERE sc.id = ic.swept_call_id +) c +WHERE ic.incident_id = @id; -- name: GetIncident :one SELECT