2024-12-19 16:14:41 -05:00
|
|
|
package callstore
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
2024-12-27 13:14:45 -05:00
|
|
|
"fmt"
|
2024-12-19 16:14:41 -05:00
|
|
|
|
|
|
|
"dynatron.me/x/stillbox/internal/common"
|
|
|
|
"dynatron.me/x/stillbox/internal/jsontypes"
|
|
|
|
|
|
|
|
"dynatron.me/x/stillbox/pkg/calls"
|
|
|
|
"dynatron.me/x/stillbox/pkg/database"
|
2025-01-18 17:22:08 -05:00
|
|
|
"dynatron.me/x/stillbox/pkg/rbac"
|
|
|
|
"dynatron.me/x/stillbox/pkg/talkgroups/tgstore"
|
|
|
|
"dynatron.me/x/stillbox/pkg/users"
|
2024-12-19 16:14:41 -05:00
|
|
|
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"github.com/jackc/pgx/v5"
|
2024-12-27 13:14:45 -05:00
|
|
|
"github.com/jackc/pgx/v5/pgtype"
|
2024-12-19 16:14:41 -05:00
|
|
|
)
|
|
|
|
|
|
|
|
type Store interface {
|
2025-01-18 17:22:08 -05:00
|
|
|
// AddCall adds a call to the database.
|
|
|
|
AddCall(ctx context.Context, call *calls.Call) error
|
|
|
|
|
|
|
|
// DeleteCall deletes a call.
|
|
|
|
Delete(ctx context.Context, id uuid.UUID) error
|
|
|
|
|
2024-12-19 16:14:41 -05:00
|
|
|
// CallAudio returns a CallAudio struct
|
|
|
|
CallAudio(ctx context.Context, id uuid.UUID) (*calls.CallAudio, error)
|
|
|
|
|
|
|
|
// Calls gets paginated Calls.
|
|
|
|
Calls(ctx context.Context, p CallsParams) (calls []database.ListCallsPRow, totalCount int, err error)
|
|
|
|
}
|
|
|
|
|
|
|
|
type store struct {
|
2025-01-18 17:22:08 -05:00
|
|
|
db database.Store
|
2024-12-19 16:14:41 -05:00
|
|
|
}
|
|
|
|
|
2025-01-18 17:22:08 -05:00
|
|
|
func NewStore(db database.Store) *store {
|
|
|
|
return &store{
|
|
|
|
db: db,
|
|
|
|
}
|
2024-12-19 16:14:41 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
type storeCtxKey string
|
|
|
|
|
|
|
|
const StoreCtxKey storeCtxKey = "store"
|
|
|
|
|
|
|
|
func CtxWithStore(ctx context.Context, s Store) context.Context {
|
|
|
|
return context.WithValue(ctx, StoreCtxKey, s)
|
|
|
|
}
|
|
|
|
|
|
|
|
func FromCtx(ctx context.Context) Store {
|
|
|
|
s, ok := ctx.Value(StoreCtxKey).(Store)
|
|
|
|
if !ok {
|
2025-01-18 17:22:08 -05:00
|
|
|
panic("no call store in context")
|
2024-12-19 16:14:41 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
return s
|
|
|
|
}
|
|
|
|
|
2025-01-18 17:22:08 -05:00
|
|
|
func toAddCallParams(call *calls.Call) database.AddCallParams {
|
|
|
|
return database.AddCallParams{
|
|
|
|
ID: call.ID,
|
|
|
|
Submitter: call.Submitter.Int32Ptr(),
|
|
|
|
System: call.System,
|
|
|
|
Talkgroup: call.Talkgroup,
|
|
|
|
CallDate: pgtype.Timestamptz{Time: call.DateTime, Valid: true},
|
|
|
|
AudioName: common.NilIfZero(call.AudioName),
|
|
|
|
AudioBlob: call.Audio,
|
|
|
|
AudioType: common.NilIfZero(call.AudioType),
|
|
|
|
Duration: call.Duration.MsInt32Ptr(),
|
|
|
|
Frequency: call.Frequency,
|
|
|
|
Frequencies: call.Frequencies,
|
|
|
|
Patches: call.Patches,
|
|
|
|
TGLabel: call.TalkgroupLabel,
|
|
|
|
TGAlphaTag: call.TGAlphaTag,
|
|
|
|
TGGroup: call.TalkgroupGroup,
|
|
|
|
Source: call.Source,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *store) AddCall(ctx context.Context, call *calls.Call) error {
|
|
|
|
_, err := rbac.Check(ctx, call, rbac.WithActions(rbac.ActionCreate))
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
params := toAddCallParams(call)
|
|
|
|
db := database.FromCtx(ctx)
|
|
|
|
tgs := tgstore.FromCtx(ctx)
|
|
|
|
|
|
|
|
err = db.InTx(ctx, func(tx database.Store) error {
|
|
|
|
err := tx.AddCall(ctx, params)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("add call: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}, pgx.TxOptions{})
|
|
|
|
|
|
|
|
if err != nil && database.IsTGConstraintViolation(err) {
|
|
|
|
return db.InTx(ctx, func(tx database.Store) error {
|
|
|
|
_, err := tgs.LearnTG(ctx, call)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("learn tg: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
err = tx.AddCall(ctx, params)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("learn tg retry: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}, pgx.TxOptions{})
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2024-12-19 16:14:41 -05:00
|
|
|
func (s *store) CallAudio(ctx context.Context, id uuid.UUID) (*calls.CallAudio, error) {
|
2025-01-18 17:22:08 -05:00
|
|
|
_, err := rbac.Check(ctx, rbac.UseResource(rbac.ResourceCall), rbac.WithActions(rbac.ActionRead))
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2024-12-19 16:14:41 -05:00
|
|
|
db := database.FromCtx(ctx)
|
|
|
|
|
|
|
|
dbCall, err := db.GetCallAudioByID(ctx, id)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return &calls.CallAudio{
|
|
|
|
CallDate: jsontypes.Time(dbCall.CallDate.Time),
|
|
|
|
AudioName: dbCall.AudioName,
|
|
|
|
AudioType: dbCall.AudioType,
|
|
|
|
AudioBlob: dbCall.AudioBlob,
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
type CallsParams struct {
|
|
|
|
common.Pagination
|
|
|
|
Direction *common.SortDirection `json:"dir"`
|
|
|
|
|
2024-12-27 13:14:45 -05:00
|
|
|
Start *jsontypes.Time `json:"start"`
|
|
|
|
End *jsontypes.Time `json:"end"`
|
|
|
|
TagsAny []string `json:"tagsAny"`
|
|
|
|
TagsNot []string `json:"tagsNot"`
|
|
|
|
TGFilter *string `json:"tgFilter"`
|
|
|
|
AtLeastSeconds *float32 `json:"atLeastSeconds"`
|
2024-12-19 16:14:41 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
func (s *store) Calls(ctx context.Context, p CallsParams) (rows []database.ListCallsPRow, totalCount int, err error) {
|
2025-01-18 17:22:08 -05:00
|
|
|
_, err = rbac.Check(ctx, rbac.UseResource(rbac.ResourceCall), rbac.WithActions(rbac.ActionRead))
|
|
|
|
if err != nil {
|
|
|
|
return nil, 0, err
|
|
|
|
}
|
|
|
|
|
2024-12-19 16:14:41 -05:00
|
|
|
db := database.FromCtx(ctx)
|
|
|
|
|
|
|
|
offset, perPage := p.Pagination.OffsetPerPage(100)
|
|
|
|
par := database.ListCallsPParams{
|
|
|
|
Start: p.Start.PGTypeTSTZ(),
|
|
|
|
End: p.End.PGTypeTSTZ(),
|
|
|
|
TagsAny: p.TagsAny,
|
|
|
|
TagsNot: p.TagsNot,
|
|
|
|
Offset: offset,
|
|
|
|
PerPage: perPage,
|
|
|
|
Direction: p.Direction.DirString(common.DirAsc),
|
2024-12-27 13:14:45 -05:00
|
|
|
TGFilter: p.TGFilter,
|
|
|
|
}
|
|
|
|
|
|
|
|
if p.AtLeastSeconds != nil {
|
|
|
|
var n pgtype.Numeric
|
|
|
|
if err := n.Scan(fmt.Sprint(*p.AtLeastSeconds * 1000)); err != nil {
|
|
|
|
return nil, 0, err
|
|
|
|
}
|
|
|
|
|
|
|
|
par.LongerThan = n
|
2024-12-19 16:14:41 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
var count int64
|
|
|
|
txErr := db.InTx(ctx, func(db database.Store) error {
|
|
|
|
var err error
|
|
|
|
count, err = db.ListCallsCount(ctx, database.ListCallsCountParams{
|
2024-12-29 13:27:17 -05:00
|
|
|
Start: par.Start,
|
|
|
|
End: par.End,
|
|
|
|
TagsAny: par.TagsAny,
|
|
|
|
TagsNot: par.TagsNot,
|
|
|
|
TGFilter: par.TGFilter,
|
|
|
|
LongerThan: par.LongerThan,
|
2024-12-19 16:14:41 -05:00
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2024-12-29 13:27:17 -05:00
|
|
|
if offset > int32(count) {
|
2024-12-29 15:24:40 -05:00
|
|
|
return common.ErrPageOutOfRange
|
2024-12-29 13:27:17 -05:00
|
|
|
}
|
|
|
|
|
2024-12-19 16:14:41 -05:00
|
|
|
rows, err = db.ListCallsP(ctx, par)
|
|
|
|
return err
|
|
|
|
}, pgx.TxOptions{})
|
|
|
|
if txErr != nil {
|
|
|
|
return nil, 0, txErr
|
|
|
|
}
|
|
|
|
|
|
|
|
return rows, int(count), err
|
|
|
|
}
|
2025-01-18 17:22:08 -05:00
|
|
|
|
|
|
|
func (s *store) Delete(ctx context.Context, id uuid.UUID) error {
|
|
|
|
callOwn, err := s.getCallOwner(ctx, id)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
_, err = rbac.Check(ctx, &callOwn, rbac.WithActions(rbac.ActionDelete))
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
return database.FromCtx(ctx).DeleteCall(ctx, id)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *store) getCallOwner(ctx context.Context, id uuid.UUID) (calls.Call, error) {
|
|
|
|
subInt, err := database.FromCtx(ctx).GetCallSubmitter(ctx, id)
|
|
|
|
|
|
|
|
var sub *users.UserID
|
|
|
|
|
|
|
|
if subInt != nil {
|
|
|
|
sub = common.PtrTo(users.UserID(*subInt))
|
|
|
|
}
|
|
|
|
return calls.Call{ID: id, Submitter: sub}, err
|
|
|
|
}
|