Compare commits

...

7 commits

Author SHA1 Message Date
3b8a50d3e8 Merge pull request 'REST and other improvements' (#27) from rest into trunk
Reviewed-on: #27
2024-11-06 20:50:39 -05:00
3f133e152a UUID is part of Call and is generated by us.
Closes #8
2024-11-06 20:47:10 -05:00
c4df60a89a Change ws route 2024-11-06 20:17:13 -05:00
c1a258f63c Change login route, add form 2024-11-06 20:13:46 -05:00
65cddbcc0d Don't apply talkgroup weight twice
Also tag ID fields
2024-11-06 14:20:15 -05:00
fb2387c212 API begin 2024-11-04 23:42:09 -05:00
170970e92d wip 2024-11-04 11:49:03 -05:00
25 changed files with 685 additions and 188 deletions

View file

@ -57,7 +57,7 @@ func main() {
loginForm.Add("username", *username) loginForm.Add("username", *username)
loginForm.Add("password", *password) loginForm.Add("password", *password)
loginReq, err := http.NewRequest("POST", "http"+secureSuffix()+"://"+*addr+"/login", strings.NewReader(loginForm.Encode())) loginReq, err := http.NewRequest("POST", "http"+secureSuffix()+"://"+*addr+"/api/login", strings.NewReader(loginForm.Encode()))
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -87,7 +87,7 @@ func main() {
log.Fatal(err) log.Fatal(err)
} }
u := url.URL{Scheme: "ws" + secureSuffix(), Host: *addr, Path: "/ws"} u := url.URL{Scheme: "ws" + secureSuffix(), Host: *addr, Path: "/api/ws"}
log.Printf("connecting to %s", u.String()) log.Printf("connecting to %s", u.String())
dialer := websocket.Dialer{ dialer := websocket.Dialer{

View file

@ -31,7 +31,7 @@ func newItem[K comparable](id K, options *options[K]) *item[K] {
} }
} }
func (i *item[K]) score(id K) Score[K] { func (i *item[K]) score() Score[K] {
recentCount, count := i.computeCounts() recentCount, count := i.computeCounts()
if recentCount < i.options.countThreshold { if recentCount < i.options.countThreshold {
return Score[K]{} return Score[K]{}

View file

@ -192,7 +192,7 @@ func (s *Scorer[K]) addToItem(item *item[K], tm time.Time) {
func (s *Scorer[K]) Score() Scores[K] { func (s *Scorer[K]) Score() Scores[K] {
var scores Scores[K] var scores Scores[K]
for id, item := range s.items { for id, item := range s.items {
score := item.score(id) score := item.score()
score.ID = id score.ID = id
scores = append(scores, score) scores = append(scores, score)
} }

View file

@ -169,12 +169,9 @@ func (as *alerter) eval(ctx context.Context, now time.Time, testMode bool) ([]Al
for _, s := range as.scores { for _, s := range as.scores {
origScore := s.Score origScore := s.Score
tgr, err := as.tgCache.TG(ctx, s.ID) tgr, err := as.tgCache.TG(ctx, s.ID)
if err == nil { if err == nil && !tgr.Talkgroup.Alert {
if !tgr.Talkgroup.Alert {
continue continue
} }
s.Score *= float64(tgr.Talkgroup.Weight)
}
if s.Score > as.cfg.AlertThreshold || testMode { if s.Score > as.cfg.AlertThreshold || testMode {
if old, inCache := as.alertCache[s.ID]; !inCache || now.Sub(old.Timestamp) > as.renotify { if old, inCache := as.alertCache[s.ID]; !inCache || now.Sub(old.Timestamp) > as.renotify {

View file

@ -69,20 +69,20 @@ func (as *alerter) tgStatsHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
db := database.FromCtx(ctx) db := database.FromCtx(ctx)
tgs, err := db.GetTalkgroupsByPackedIDs(ctx, as.packedScoredTGs()) tgs, err := db.GetTalkgroupsWithLearnedByPackedIDs(ctx, as.packedScoredTGs())
if err != nil { if err != nil {
log.Error().Err(err).Msg("stats TG get failed") log.Error().Err(err).Msg("stats TG get failed")
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
tgMap := make(map[talkgroups.ID]database.GetTalkgroupsByPackedIDsRow, len(tgs)) tgMap := make(map[talkgroups.ID]database.GetTalkgroupsWithLearnedByPackedIDsRow, len(tgs))
for _, t := range tgs { for _, t := range tgs {
tgMap[talkgroups.ID{System: uint32(t.System.ID), Talkgroup: uint32(t.Talkgroup.ID)}] = t tgMap[talkgroups.ID{System: uint32(t.System.ID), Talkgroup: uint32(t.Talkgroup.Tgid)}] = t
} }
renderData := struct { renderData := struct {
TGs map[talkgroups.ID]database.GetTalkgroupsByPackedIDsRow TGs map[talkgroups.ID]database.GetTalkgroupsWithLearnedByPackedIDsRow
Scores trending.Scores[talkgroups.ID] Scores trending.Scores[talkgroups.ID]
LastScore time.Time LastScore time.Time
Simulation *Simulation Simulation *Simulation

127
pkg/api/api.go Normal file
View file

@ -0,0 +1,127 @@
package api
import (
"encoding/json"
"errors"
"net/http"
"dynatron.me/x/stillbox/pkg/talkgroups"
"github.com/go-chi/chi/v5"
"github.com/go-viper/mapstructure/v2"
"github.com/jackc/pgx/v5"
"github.com/rs/zerolog/log"
)
type API interface {
Subrouter() http.Handler
}
type api struct {
tgs talkgroups.Store
}
func New(tgs talkgroups.Store) API {
s := &api{
tgs: tgs,
}
return s
}
func (a *api) Subrouter() http.Handler {
r := chi.NewMux()
r.Get("/talkgroup/{system:\\d+}/{id:\\d+}", a.talkgroup)
r.Get("/talkgroup/{system:\\d+}/", a.talkgroup)
r.Get("/talkgroup/", a.talkgroup)
return r
}
var statusMapping = map[error]int{
talkgroups.ErrNotFound: http.StatusNotFound,
pgx.ErrNoRows: http.StatusNotFound,
}
func httpCode(err error) int {
c, ok := statusMapping[err]
if ok {
return c
}
for e, c := range statusMapping { // check if err wraps an error we know about
if errors.Is(err, e) {
return c
}
}
return http.StatusInternalServerError
}
func (a *api) writeResponse(w http.ResponseWriter, r *http.Request, data interface{}, err error) {
if err != nil {
log.Error().Str("path", r.URL.Path).Err(err).Msg("request failed")
http.Error(w, err.Error(), httpCode(err))
return
}
w.Header().Set("Content-Type", "application/json")
enc := json.NewEncoder(w)
err = enc.Encode(data)
if err != nil {
log.Error().Str("path", r.URL.Path).Err(err).Msg("response marshal failed")
http.Error(w, err.Error(), httpCode(err))
return
}
}
func decodeParams(d interface{}, r *http.Request) error {
params := chi.RouteContext(r.Context()).URLParams
m := make(map[string]string, len(params.Keys))
for i, k := range params.Keys {
m[k] = params.Values[i]
}
dec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
Metadata: nil,
Result: d,
TagName: "param",
WeaklyTypedInput: true,
})
if err != nil {
return err
}
return dec.Decode(m)
}
func (a *api) badReq(w http.ResponseWriter, err error) {
http.Error(w, err.Error(), http.StatusBadRequest)
}
func (a *api) talkgroup(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
p := struct {
System *int `param:"system"`
ID *int `param:"id"`
}{}
err := decodeParams(&p, r)
if err != nil {
a.badReq(w, err)
return
}
var res interface{}
switch {
case p.System != nil && p.ID != nil:
res, err = a.tgs.TG(ctx, talkgroups.TG(*p.System, *p.ID))
case p.System != nil:
res, err = a.tgs.SystemTGs(ctx, int32(*p.System))
default:
res, err = a.tgs.TGs(ctx, nil)
}
a.writeResponse(w, r, res, err)
}

View file

@ -4,7 +4,10 @@ import (
"errors" "errors"
"net/http" "net/http"
_ "embed"
"dynatron.me/x/stillbox/pkg/config" "dynatron.me/x/stillbox/pkg/config"
"github.com/go-chi/chi/v5"
"github.com/go-chi/jwtauth/v5" "github.com/go-chi/jwtauth/v5"
) )
@ -66,3 +69,20 @@ func ErrorResponse(w http.ResponseWriter, err error) {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
} }
} }
func (a *Auth) PublicRoutes(r chi.Router) {
r.Post("/api/login", a.routeAuth)
r.Get("/api/login", a.routeLogin)
}
func (a *Auth) PrivateRoutes(r chi.Router) {
r.Get("/refresh", a.routeRefresh)
}
//go:embed login.html
var loginPage []byte
func (a *Auth) routeLogin(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Content-Type", "text/html")
_, _ = w.Write(loginPage)
}

View file

@ -110,14 +110,6 @@ func (a *Auth) newToken(uid int32) string {
return tokenString return tokenString
} }
func (a *Auth) PublicRoutes(r chi.Router) {
r.Post("/login", a.routeAuth)
}
func (a *Auth) PrivateRoutes(r chi.Router) {
r.Get("/refresh", a.routeRefresh)
}
func (a *Auth) allowInsecureCookie(r *http.Request) bool { func (a *Auth) allowInsecureCookie(r *http.Request) bool {
host := strings.Split(r.Host, ":") host := strings.Split(r.Host, ":")
v, has := a.cfg.AllowInsecure[host[0]] v, has := a.cfg.AllowInsecure[host[0]]

17
pkg/auth/login.html Normal file
View file

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html>
<head>
<title>Login</title>
</head>
<body>
<div>
<form action="/login" method="POST">
<label for="username">Username: </label>
<input type="text" name="username" />
<label for="password">Password: </label>
<input type="password" name="password" />
<input type="submit" value="Login" />
</form>
</div>
</body>
</html>

View file

@ -9,6 +9,7 @@ import (
"dynatron.me/x/stillbox/pkg/pb" "dynatron.me/x/stillbox/pkg/pb"
"dynatron.me/x/stillbox/pkg/talkgroups" "dynatron.me/x/stillbox/pkg/talkgroups"
"github.com/google/uuid"
"google.golang.org/protobuf/types/known/timestamppb" "google.golang.org/protobuf/types/known/timestamppb"
) )
@ -32,6 +33,7 @@ func (d CallDuration) Seconds() int32 {
} }
type Call struct { type Call struct {
ID uuid.UUID
Audio []byte Audio []byte
AudioName string AudioName string
AudioType string AudioType string
@ -68,6 +70,7 @@ func Make(call *Call, dontStore bool) (*Call, error) {
} }
call.shouldStore = dontStore call.shouldStore = dontStore
call.ID = uuid.New()
return call, nil return call, nil
} }
@ -92,6 +95,7 @@ func toInt32Slice(s []int) []int32 {
func (c *Call) ToPB() *pb.Call { func (c *Call) ToPB() *pb.Call {
return &pb.Call{ return &pb.Call{
Id: c.ID.String(),
AudioName: c.AudioName, AudioName: c.AudioName,
AudioType: c.AudioType, AudioType: c.AudioType,
DateTime: timestamppb.New(c.DateTime), DateTime: timestamppb.New(c.DateTime),

View file

@ -52,7 +52,7 @@ func (q *Queries) AddAlert(ctx context.Context, arg AddAlertParams) error {
return err return err
} }
const addCall = `-- name: AddCall :one const addCall = `-- name: AddCall :exec
INSERT INTO calls ( INSERT INTO calls (
id, id,
submitter, submitter,
@ -71,11 +71,29 @@ INSERT INTO calls (
tg_alpha_tag, tg_alpha_tag,
tg_group, tg_group,
source source
) VALUES (gen_random_uuid(), $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16) ) VALUES (
RETURNING id $1,
$2,
$3,
$4,
$5,
$6,
$7,
$8,
$9,
$10,
$11,
$12,
$13,
$14,
$15,
$16,
$17
)
` `
type AddCallParams struct { type AddCallParams struct {
ID uuid.UUID `json:"id"`
Submitter *int32 `json:"submitter"` Submitter *int32 `json:"submitter"`
System int `json:"system"` System int `json:"system"`
Talkgroup int `json:"talkgroup"` Talkgroup int `json:"talkgroup"`
@ -94,8 +112,9 @@ type AddCallParams struct {
Source int `json:"source"` Source int `json:"source"`
} }
func (q *Queries) AddCall(ctx context.Context, arg AddCallParams) (uuid.UUID, error) { func (q *Queries) AddCall(ctx context.Context, arg AddCallParams) error {
row := q.db.QueryRow(ctx, addCall, _, err := q.db.Exec(ctx, addCall,
arg.ID,
arg.Submitter, arg.Submitter,
arg.System, arg.System,
arg.Talkgroup, arg.Talkgroup,
@ -113,9 +132,7 @@ func (q *Queries) AddCall(ctx context.Context, arg AddCallParams) (uuid.UUID, er
arg.TgGroup, arg.TgGroup,
arg.Source, arg.Source,
) )
var id uuid.UUID return err
err := row.Scan(&id)
return id, err
} }
const getDatabaseSize = `-- name: GetDatabaseSize :one const getDatabaseSize = `-- name: GetDatabaseSize :one

11
pkg/database/extend.go Normal file
View file

@ -0,0 +1,11 @@
package database
func (d GetTalkgroupsWithLearnedByPackedIDsRow) GetTalkgroup() Talkgroup { return d.Talkgroup }
func (d GetTalkgroupsWithLearnedByPackedIDsRow) GetSystem() System { return d.System }
func (d GetTalkgroupsWithLearnedByPackedIDsRow) GetLearned() bool { return d.Learned }
func (g GetTalkgroupsWithLearnedRow) GetTalkgroup() Talkgroup { return g.Talkgroup }
func (g GetTalkgroupsWithLearnedRow) GetSystem() System { return g.System }
func (g GetTalkgroupsWithLearnedRow) GetLearned() bool { return g.Learned }
func (g GetTalkgroupsWithLearnedBySystemRow) GetTalkgroup() Talkgroup { return g.Talkgroup }
func (g GetTalkgroupsWithLearnedBySystemRow) GetSystem() System { return g.System }
func (g GetTalkgroupsWithLearnedBySystemRow) GetLearned() bool { return g.Learned }

View file

@ -13,7 +13,7 @@ import (
type Querier interface { type Querier interface {
AddAlert(ctx context.Context, arg AddAlertParams) error AddAlert(ctx context.Context, arg AddAlertParams) error
AddCall(ctx context.Context, arg AddCallParams) (uuid.UUID, error) AddCall(ctx context.Context, arg AddCallParams) error
BulkSetTalkgroupTags(ctx context.Context, iD int64, tags []string) error BulkSetTalkgroupTags(ctx context.Context, iD int64, tags []string) error
CreateAPIKey(ctx context.Context, owner int, expires pgtype.Timestamp, disabled *bool) (ApiKey, error) CreateAPIKey(ctx context.Context, owner int, expires pgtype.Timestamp, disabled *bool) (ApiKey, error)
CreateUser(ctx context.Context, arg CreateUserParams) (User, error) CreateUser(ctx context.Context, arg CreateUserParams) (User, error)
@ -26,10 +26,12 @@ type Querier interface {
GetTalkgroupIDsByTags(ctx context.Context, anytags []string, alltags []string, nottags []string) ([]GetTalkgroupIDsByTagsRow, error) GetTalkgroupIDsByTags(ctx context.Context, anytags []string, alltags []string, nottags []string) ([]GetTalkgroupIDsByTagsRow, error)
GetTalkgroupTags(ctx context.Context, sys int, tg int) ([]string, error) GetTalkgroupTags(ctx context.Context, sys int, tg int) ([]string, error)
GetTalkgroupWithLearned(ctx context.Context, systemID int, tgid int) (GetTalkgroupWithLearnedRow, error) GetTalkgroupWithLearned(ctx context.Context, systemID int, tgid int) (GetTalkgroupWithLearnedRow, error)
GetTalkgroupWithLearnedByPackedIDs(ctx context.Context, dollar_1 []int64) ([]GetTalkgroupWithLearnedByPackedIDsRow, error)
GetTalkgroupsByPackedIDs(ctx context.Context, dollar_1 []int64) ([]GetTalkgroupsByPackedIDsRow, error) GetTalkgroupsByPackedIDs(ctx context.Context, dollar_1 []int64) ([]GetTalkgroupsByPackedIDsRow, error)
GetTalkgroupsWithAllTags(ctx context.Context, tags []string) ([]GetTalkgroupsWithAllTagsRow, error) GetTalkgroupsWithAllTags(ctx context.Context, tags []string) ([]GetTalkgroupsWithAllTagsRow, error)
GetTalkgroupsWithAnyTags(ctx context.Context, tags []string) ([]GetTalkgroupsWithAnyTagsRow, error) GetTalkgroupsWithAnyTags(ctx context.Context, tags []string) ([]GetTalkgroupsWithAnyTagsRow, error)
GetTalkgroupsWithLearned(ctx context.Context) ([]GetTalkgroupsWithLearnedRow, error)
GetTalkgroupsWithLearnedByPackedIDs(ctx context.Context, dollar_1 []int64) ([]GetTalkgroupsWithLearnedByPackedIDsRow, error)
GetTalkgroupsWithLearnedBySystem(ctx context.Context, system int32) ([]GetTalkgroupsWithLearnedBySystemRow, error)
GetUserByID(ctx context.Context, id int32) (User, error) GetUserByID(ctx context.Context, id int32) (User, error)
GetUserByUID(ctx context.Context, id int32) (User, error) GetUserByUID(ctx context.Context, id int32) (User, error)
GetUserByUsername(ctx context.Context, username string) (User, error) GetUserByUsername(ctx context.Context, username string) (User, error)

View file

@ -151,67 +151,6 @@ func (q *Queries) GetTalkgroupWithLearned(ctx context.Context, systemID int, tgi
return i, err return i, err
} }
const getTalkgroupWithLearnedByPackedIDs = `-- name: GetTalkgroupWithLearnedByPackedIDs :many
SELECT
tg.id, tg.system_id, tg.tgid, tg.name, tg.alpha_tag, tg.tg_group, tg.frequency, tg.metadata, tg.tags, tg.alert, tg.alert_config, tg.weight, sys.id, sys.name,
FALSE learned
FROM talkgroups tg
JOIN systems sys ON tg.system_id = sys.id
WHERE tg.id = ANY($1::INT8[])
UNION
SELECT
tgl.id::INT8, tgl.system_id::INT4, tgl.tgid::INT4, tgl.name,
tgl.alpha_tag, tgl.alpha_tag, NULL::INTEGER, NULL::JSONB,
CASE WHEN tgl.alpha_tag IS NULL THEN NULL ELSE ARRAY[tgl.alpha_tag] END,
TRUE, NULL::JSONB, 1.0, sys.id, sys.name,
TRUE learned
FROM talkgroups_learned tgl
JOIN systems sys ON tgl.system_id = sys.id
WHERE systg2id(tgl.system_id, tgl.tgid) = ANY($1::INT8[]) AND ignored IS NOT TRUE
`
type GetTalkgroupWithLearnedByPackedIDsRow struct {
Talkgroup Talkgroup `json:"talkgroup"`
System System `json:"system"`
Learned bool `json:"learned"`
}
func (q *Queries) GetTalkgroupWithLearnedByPackedIDs(ctx context.Context, dollar_1 []int64) ([]GetTalkgroupWithLearnedByPackedIDsRow, error) {
rows, err := q.db.Query(ctx, getTalkgroupWithLearnedByPackedIDs, dollar_1)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetTalkgroupWithLearnedByPackedIDsRow
for rows.Next() {
var i GetTalkgroupWithLearnedByPackedIDsRow
if err := rows.Scan(
&i.Talkgroup.ID,
&i.Talkgroup.SystemID,
&i.Talkgroup.Tgid,
&i.Talkgroup.Name,
&i.Talkgroup.AlphaTag,
&i.Talkgroup.TgGroup,
&i.Talkgroup.Frequency,
&i.Talkgroup.Metadata,
&i.Talkgroup.Tags,
&i.Talkgroup.Alert,
&i.Talkgroup.AlertConfig,
&i.Talkgroup.Weight,
&i.System.ID,
&i.System.Name,
&i.Learned,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getTalkgroupsByPackedIDs = `-- name: GetTalkgroupsByPackedIDs :many const getTalkgroupsByPackedIDs = `-- name: GetTalkgroupsByPackedIDs :many
SELECT tg.id, tg.system_id, tg.tgid, tg.name, tg.alpha_tag, tg.tg_group, tg.frequency, tg.metadata, tg.tags, tg.alert, tg.alert_config, tg.weight, sys.id, sys.name FROM talkgroups tg SELECT tg.id, tg.system_id, tg.tgid, tg.name, tg.alpha_tag, tg.tg_group, tg.frequency, tg.metadata, tg.tags, tg.alert, tg.alert_config, tg.weight, sys.id, sys.name FROM talkgroups tg
JOIN systems sys ON tg.system_id = sys.id JOIN systems sys ON tg.system_id = sys.id
@ -342,6 +281,188 @@ func (q *Queries) GetTalkgroupsWithAnyTags(ctx context.Context, tags []string) (
return items, nil return items, nil
} }
const getTalkgroupsWithLearned = `-- name: GetTalkgroupsWithLearned :many
SELECT
tg.id, tg.system_id, tg.tgid, tg.name, tg.alpha_tag, tg.tg_group, tg.frequency, tg.metadata, tg.tags, tg.alert, tg.alert_config, tg.weight, sys.id, sys.name,
FALSE learned
FROM talkgroups tg
JOIN systems sys ON tg.system_id = sys.id
UNION
SELECT
tgl.id::INT8, tgl.system_id::INT4, tgl.tgid::INT4, tgl.name,
tgl.alpha_tag, tgl.alpha_tag, NULL::INTEGER, NULL::JSONB,
CASE WHEN tgl.alpha_tag IS NULL THEN NULL ELSE ARRAY[tgl.alpha_tag] END,
TRUE, NULL::JSONB, 1.0, sys.id, sys.name,
TRUE learned
FROM talkgroups_learned tgl
JOIN systems sys ON tgl.system_id = sys.id
WHERE ignored IS NOT TRUE
`
type GetTalkgroupsWithLearnedRow struct {
Talkgroup Talkgroup `json:"talkgroup"`
System System `json:"system"`
Learned bool `json:"learned"`
}
func (q *Queries) GetTalkgroupsWithLearned(ctx context.Context) ([]GetTalkgroupsWithLearnedRow, error) {
rows, err := q.db.Query(ctx, getTalkgroupsWithLearned)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetTalkgroupsWithLearnedRow
for rows.Next() {
var i GetTalkgroupsWithLearnedRow
if err := rows.Scan(
&i.Talkgroup.ID,
&i.Talkgroup.SystemID,
&i.Talkgroup.Tgid,
&i.Talkgroup.Name,
&i.Talkgroup.AlphaTag,
&i.Talkgroup.TgGroup,
&i.Talkgroup.Frequency,
&i.Talkgroup.Metadata,
&i.Talkgroup.Tags,
&i.Talkgroup.Alert,
&i.Talkgroup.AlertConfig,
&i.Talkgroup.Weight,
&i.System.ID,
&i.System.Name,
&i.Learned,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getTalkgroupsWithLearnedByPackedIDs = `-- name: GetTalkgroupsWithLearnedByPackedIDs :many
SELECT
tg.id, tg.system_id, tg.tgid, tg.name, tg.alpha_tag, tg.tg_group, tg.frequency, tg.metadata, tg.tags, tg.alert, tg.alert_config, tg.weight, sys.id, sys.name,
FALSE learned
FROM talkgroups tg
JOIN systems sys ON tg.system_id = sys.id
WHERE tg.id = ANY($1::INT8[])
UNION
SELECT
tgl.id::INT8, tgl.system_id::INT4, tgl.tgid::INT4, tgl.name,
tgl.alpha_tag, tgl.alpha_tag, NULL::INTEGER, NULL::JSONB,
CASE WHEN tgl.alpha_tag IS NULL THEN NULL ELSE ARRAY[tgl.alpha_tag] END,
TRUE, NULL::JSONB, 1.0, sys.id, sys.name,
TRUE learned
FROM talkgroups_learned tgl
JOIN systems sys ON tgl.system_id = sys.id
WHERE systg2id(tgl.system_id, tgl.tgid) = ANY($1::INT8[]) AND ignored IS NOT TRUE
`
type GetTalkgroupsWithLearnedByPackedIDsRow struct {
Talkgroup Talkgroup `json:"talkgroup"`
System System `json:"system"`
Learned bool `json:"learned"`
}
func (q *Queries) GetTalkgroupsWithLearnedByPackedIDs(ctx context.Context, dollar_1 []int64) ([]GetTalkgroupsWithLearnedByPackedIDsRow, error) {
rows, err := q.db.Query(ctx, getTalkgroupsWithLearnedByPackedIDs, dollar_1)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetTalkgroupsWithLearnedByPackedIDsRow
for rows.Next() {
var i GetTalkgroupsWithLearnedByPackedIDsRow
if err := rows.Scan(
&i.Talkgroup.ID,
&i.Talkgroup.SystemID,
&i.Talkgroup.Tgid,
&i.Talkgroup.Name,
&i.Talkgroup.AlphaTag,
&i.Talkgroup.TgGroup,
&i.Talkgroup.Frequency,
&i.Talkgroup.Metadata,
&i.Talkgroup.Tags,
&i.Talkgroup.Alert,
&i.Talkgroup.AlertConfig,
&i.Talkgroup.Weight,
&i.System.ID,
&i.System.Name,
&i.Learned,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getTalkgroupsWithLearnedBySystem = `-- name: GetTalkgroupsWithLearnedBySystem :many
SELECT
tg.id, tg.system_id, tg.tgid, tg.name, tg.alpha_tag, tg.tg_group, tg.frequency, tg.metadata, tg.tags, tg.alert, tg.alert_config, tg.weight, sys.id, sys.name,
FALSE learned
FROM talkgroups tg
JOIN systems sys ON tg.system_id = sys.id
WHERE tg.system_id = $1
UNION
SELECT
tgl.id::INT8, tgl.system_id::INT4, tgl.tgid::INT4, tgl.name,
tgl.alpha_tag, tgl.alpha_tag, NULL::INTEGER, NULL::JSONB,
CASE WHEN tgl.alpha_tag IS NULL THEN NULL ELSE ARRAY[tgl.alpha_tag] END,
TRUE, NULL::JSONB, 1.0, sys.id, sys.name,
TRUE learned
FROM talkgroups_learned tgl
JOIN systems sys ON tgl.system_id = sys.id
WHERE tgl.system_id = $1 AND ignored IS NOT TRUE
`
type GetTalkgroupsWithLearnedBySystemRow struct {
Talkgroup Talkgroup `json:"talkgroup"`
System System `json:"system"`
Learned bool `json:"learned"`
}
func (q *Queries) GetTalkgroupsWithLearnedBySystem(ctx context.Context, system int32) ([]GetTalkgroupsWithLearnedBySystemRow, error) {
rows, err := q.db.Query(ctx, getTalkgroupsWithLearnedBySystem, system)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetTalkgroupsWithLearnedBySystemRow
for rows.Next() {
var i GetTalkgroupsWithLearnedBySystemRow
if err := rows.Scan(
&i.Talkgroup.ID,
&i.Talkgroup.SystemID,
&i.Talkgroup.Tgid,
&i.Talkgroup.Name,
&i.Talkgroup.AlphaTag,
&i.Talkgroup.TgGroup,
&i.Talkgroup.Frequency,
&i.Talkgroup.Metadata,
&i.Talkgroup.Tags,
&i.Talkgroup.Alert,
&i.Talkgroup.AlertConfig,
&i.Talkgroup.Weight,
&i.System.ID,
&i.System.Name,
&i.Learned,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const setTalkgroupTags = `-- name: SetTalkgroupTags :exec const setTalkgroupTags = `-- name: SetTalkgroupTags :exec
UPDATE talkgroups SET tags = $3 UPDATE talkgroups SET tags = $3
WHERE id = systg2id($1, $2) WHERE id = systg2id($1, $2)

View file

@ -43,7 +43,46 @@ JOIN systems sys ON tgl.system_id = sys.id
WHERE tgl.system_id = $1 AND tgl.tgid = $2 AND ignored IS NOT TRUE WHERE tgl.system_id = $1 AND tgl.tgid = $2 AND ignored IS NOT TRUE
` `
const getTalkgroupsWithLearnedBySystemTest = `-- name: GetTalkgroupsWithLearnedBySystem :many
SELECT
tg.id, tg.system_id, tg.tgid, tg.name, tg.alpha_tag, tg.tg_group, tg.frequency, tg.metadata, tg.tags, tg.alert, tg.alert_config, tg.weight, sys.id, sys.name,
FALSE learned
FROM talkgroups tg
JOIN systems sys ON tg.system_id = sys.id
WHERE tg.system_id = $1
UNION
SELECT
tgl.id::INT8, tgl.system_id::INT4, tgl.tgid::INT4, tgl.name,
tgl.alpha_tag, tgl.alpha_tag, NULL::INTEGER, NULL::JSONB,
CASE WHEN tgl.alpha_tag IS NULL THEN NULL ELSE ARRAY[tgl.alpha_tag] END,
TRUE, NULL::JSONB, 1.0, sys.id, sys.name,
TRUE learned
FROM talkgroups_learned tgl
JOIN systems sys ON tgl.system_id = sys.id
WHERE tg.system_id = $1 AND ignored IS NOT TRUE
`
const getTalkgroupsWithLearnedTest = `-- name: GetTalkgroupsWithLearned :many
SELECT
tg.id, tg.system_id, tg.tgid, tg.name, tg.alpha_tag, tg.tg_group, tg.frequency, tg.metadata, tg.tags, tg.alert, tg.alert_config, tg.weight, sys.id, sys.name,
FALSE learned
FROM talkgroups tg
JOIN systems sys ON tg.system_id = sys.id
UNION
SELECT
tgl.id::INT8, tgl.system_id::INT4, tgl.tgid::INT4, tgl.name,
tgl.alpha_tag, tgl.alpha_tag, NULL::INTEGER, NULL::JSONB,
CASE WHEN tgl.alpha_tag IS NULL THEN NULL ELSE ARRAY[tgl.alpha_tag] END,
TRUE, NULL::JSONB, 1.0, sys.id, sys.name,
TRUE learned
FROM talkgroups_learned tgl
JOIN systems sys ON tgl.system_id = sys.id
WHERE ignored IS NOT TRUE
`
func TestQueryColumnsMatch(t *testing.T) { func TestQueryColumnsMatch(t *testing.T) {
require.Equal(t, getTalkgroupWithLearnedByPackedIDsTest, getTalkgroupWithLearnedByPackedIDs) require.Equal(t, getTalkgroupsWithLearnedByPackedIDsTest, getTalkgroupWithLearnedByPackedIDs)
require.Equal(t, getTalkgroupWithLearnedTest, getTalkgroupWithLearned) require.Equal(t, getTalkgroupWithLearnedTest, getTalkgroupWithLearned)
require.Equal(t, getTalkgroupsWithLearnedBySystemTest, getTalkgroupsWithLearnedBySystem)
require.Equal(t, getTalkgroupsWithLearnedTest, getTalkgroupsWithLearned)
} }

View file

@ -183,5 +183,5 @@ func (conn *wsConn) writeToClient(w io.WriteCloser, msg ToClient) {
} }
func (n *wsManager) PrivateRoutes(r chi.Router) { func (n *wsManager) PrivateRoutes(r chi.Router) {
r.HandleFunc("/ws", n.serveWS) r.HandleFunc("/api/ws", n.serveWS)
} }

View file

@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.33.0 // protoc-gen-go v1.33.0
// protoc v5.28.2 // protoc v5.28.3
// source: stillbox.proto // source: stillbox.proto
package pb package pb
@ -288,18 +288,19 @@ type Call struct {
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
AudioName string `protobuf:"bytes,1,opt,name=audioName,proto3" json:"audioName,omitempty"` Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
AudioType string `protobuf:"bytes,2,opt,name=audioType,proto3" json:"audioType,omitempty"` AudioName string `protobuf:"bytes,2,opt,name=audioName,proto3" json:"audioName,omitempty"`
DateTime *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=date_time,json=dateTime,proto3" json:"date_time,omitempty"` AudioType string `protobuf:"bytes,3,opt,name=audioType,proto3" json:"audioType,omitempty"`
System int32 `protobuf:"varint,4,opt,name=system,proto3" json:"system,omitempty"` DateTime *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=date_time,json=dateTime,proto3" json:"date_time,omitempty"`
Talkgroup int32 `protobuf:"varint,5,opt,name=talkgroup,proto3" json:"talkgroup,omitempty"` System int32 `protobuf:"varint,5,opt,name=system,proto3" json:"system,omitempty"`
Source int32 `protobuf:"varint,6,opt,name=source,proto3" json:"source,omitempty"` Talkgroup int32 `protobuf:"varint,6,opt,name=talkgroup,proto3" json:"talkgroup,omitempty"`
Frequency int64 `protobuf:"varint,7,opt,name=frequency,proto3" json:"frequency,omitempty"` Source int32 `protobuf:"varint,7,opt,name=source,proto3" json:"source,omitempty"`
Frequencies []int64 `protobuf:"varint,8,rep,packed,name=frequencies,proto3" json:"frequencies,omitempty"` Frequency int64 `protobuf:"varint,8,opt,name=frequency,proto3" json:"frequency,omitempty"`
Patches []int32 `protobuf:"varint,9,rep,packed,name=patches,proto3" json:"patches,omitempty"` Frequencies []int64 `protobuf:"varint,9,rep,packed,name=frequencies,proto3" json:"frequencies,omitempty"`
Sources []int32 `protobuf:"varint,10,rep,packed,name=sources,proto3" json:"sources,omitempty"` Patches []int32 `protobuf:"varint,10,rep,packed,name=patches,proto3" json:"patches,omitempty"`
Duration *int32 `protobuf:"varint,11,opt,name=duration,proto3,oneof" json:"duration,omitempty"` Sources []int32 `protobuf:"varint,11,rep,packed,name=sources,proto3" json:"sources,omitempty"`
Audio []byte `protobuf:"bytes,12,opt,name=audio,proto3" json:"audio,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"`
} }
func (x *Call) Reset() { func (x *Call) Reset() {
@ -334,6 +335,13 @@ func (*Call) Descriptor() ([]byte, []int) {
return file_stillbox_proto_rawDescGZIP(), []int{2} return file_stillbox_proto_rawDescGZIP(), []int{2}
} }
func (x *Call) GetId() string {
if x != nil {
return x.Id
}
return ""
}
func (x *Call) GetAudioName() string { func (x *Call) GetAudioName() string {
if x != nil { if x != nil {
return x.AudioName return x.AudioName
@ -1187,29 +1195,30 @@ var file_stillbox_proto_rawDesc = []byte{
0x6f, 0x75, 0x70, 0x49, 0x6e, 0x66, 0x6f, 0x48, 0x00, 0x52, 0x06, 0x74, 0x67, 0x49, 0x6e, 0x66, 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, 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, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x0d, 0x0a, 0x0b, 0x5f, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e,
0x64, 0x5f, 0x69, 0x64, 0x22, 0x81, 0x03, 0x0a, 0x04, 0x43, 0x61, 0x6c, 0x6c, 0x12, 0x1c, 0x0a, 0x64, 0x5f, 0x69, 0x64, 0x22, 0x91, 0x03, 0x0a, 0x04, 0x43, 0x61, 0x6c, 0x6c, 0x12, 0x0e, 0x0a,
0x09, 0x61, 0x75, 0x64, 0x69, 0x6f, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 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, 0x52, 0x09, 0x61, 0x75, 0x64, 0x69, 0x6f, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x61,
0x75, 0x64, 0x69, 0x6f, 0x54, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x75, 0x64, 0x69, 0x6f, 0x54, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09,
0x61, 0x75, 0x64, 0x69, 0x6f, 0x54, 0x79, 0x70, 0x65, 0x12, 0x37, 0x0a, 0x09, 0x64, 0x61, 0x74, 0x61, 0x75, 0x64, 0x69, 0x6f, 0x54, 0x79, 0x70, 0x65, 0x12, 0x37, 0x0a, 0x09, 0x64, 0x61, 0x74,
0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67,
0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 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, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x08, 0x64, 0x61, 0x74, 0x65, 0x54, 0x69,
0x6d, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x18, 0x04, 0x20, 0x01, 0x6d, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x18, 0x05, 0x20, 0x01,
0x28, 0x05, 0x52, 0x06, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x61, 0x28, 0x05, 0x52, 0x06, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x61,
0x6c, 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x18, 0x05, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x74, 0x6c, 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x18, 0x06, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x74,
0x61, 0x6c, 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x6f, 0x75, 0x72, 0x61, 0x6c, 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x6f, 0x75, 0x72,
0x63, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x63, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65,
0x12, 0x1c, 0x0a, 0x09, 0x66, 0x72, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x79, 0x18, 0x07, 0x20, 0x12, 0x1c, 0x0a, 0x09, 0x66, 0x72, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x79, 0x18, 0x08, 0x20,
0x01, 0x28, 0x03, 0x52, 0x09, 0x66, 0x72, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x79, 0x12, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x66, 0x72, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x79, 0x12, 0x20,
0x0a, 0x0b, 0x66, 0x72, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x69, 0x65, 0x73, 0x18, 0x08, 0x20, 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, 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, 0x09, 0x20, 0x03, 0x28, 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, 0x05, 0x52, 0x07, 0x70, 0x61, 0x74, 0x63, 0x68, 0x65, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x6f,
0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x05, 0x52, 0x07, 0x73, 0x6f, 0x75, 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, 0x72, 0x63, 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, 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, 0x0c, 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, 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, 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, 0x6f, 0x12, 0x35, 0x0a, 0x0b, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, 0x69, 0x6e, 0x66, 0x6f,

View file

@ -24,18 +24,19 @@ message CommandResponse {
} }
message Call { message Call {
string audioName = 1; string id = 1;
string audioType = 2; string audioName = 2;
google.protobuf.Timestamp date_time = 3; string audioType = 3;
int32 system = 4; google.protobuf.Timestamp date_time = 4;
int32 talkgroup = 5; int32 system = 5;
int32 source = 6; int32 talkgroup = 6;
int64 frequency = 7; int32 source = 7;
repeated int64 frequencies = 8; int64 frequency = 8;
repeated int32 patches = 9; repeated int64 frequencies = 9;
repeated int32 sources = 10; repeated int32 patches = 10;
optional int32 duration = 11; repeated int32 sources = 11;
bytes audio = 12; optional int32 duration = 12;
bytes audio = 13;
} }
message Hello { message Hello {

View file

@ -36,6 +36,7 @@ func (s *Server) setupRoutes() {
s.nex.PrivateRoutes(r) s.nex.PrivateRoutes(r)
s.auth.PrivateRoutes(r) s.auth.PrivateRoutes(r)
s.alerter.PrivateRoutes(r) s.alerter.PrivateRoutes(r)
r.Mount("/api", s.api.Subrouter())
}) })
r.Group(func(r chi.Router) { r.Group(func(r chi.Router) {

View file

@ -7,6 +7,7 @@ import (
"time" "time"
"dynatron.me/x/stillbox/pkg/alerting" "dynatron.me/x/stillbox/pkg/alerting"
"dynatron.me/x/stillbox/pkg/api"
"dynatron.me/x/stillbox/pkg/auth" "dynatron.me/x/stillbox/pkg/auth"
"dynatron.me/x/stillbox/pkg/config" "dynatron.me/x/stillbox/pkg/config"
"dynatron.me/x/stillbox/pkg/database" "dynatron.me/x/stillbox/pkg/database"
@ -36,6 +37,7 @@ type Server struct {
notifier notify.Notifier notifier notify.Notifier
hup chan os.Signal hup chan os.Signal
tgs talkgroups.Store tgs talkgroups.Store
api api.API
} }
func New(ctx context.Context, cfg *config.Config) (*Server, error) { func New(ctx context.Context, cfg *config.Config) (*Server, error) {
@ -59,6 +61,7 @@ func New(ctx context.Context, cfg *config.Config) (*Server, error) {
} }
tgCache := talkgroups.NewCache() tgCache := talkgroups.NewCache()
api := api.New(tgCache)
srv := &Server{ srv := &Server{
auth: authenticator, auth: authenticator,
@ -70,6 +73,7 @@ func New(ctx context.Context, cfg *config.Config) (*Server, error) {
alerter: alerting.New(cfg.Alerting, tgCache, alerting.WithNotifier(notifier)), alerter: alerting.New(cfg.Alerting, tgCache, alerting.WithNotifier(notifier)),
notifier: notifier, notifier: notifier,
tgs: tgCache, tgs: tgCache,
api: api,
} }
srv.sinks.Register("database", sinks.NewDatabaseSink(srv.db), true) srv.sinks.Register("database", sinks.NewDatabaseSink(srv.db), true)

View file

@ -26,12 +26,12 @@ func (s *DatabaseSink) Call(ctx context.Context, call *calls.Call) error {
return nil return nil
} }
dbCall, err := s.db.AddCall(ctx, s.toAddCallParams(call)) err := s.db.AddCall(ctx, s.toAddCallParams(call))
if err != nil { if err != nil {
return fmt.Errorf("add call: %w", err) return fmt.Errorf("add call: %w", err)
} }
log.Debug().Str("id", dbCall.String()).Int("system", call.System).Int("tgid", call.Talkgroup).Msg("stored") log.Debug().Str("id", call.ID.String()).Int("system", call.System).Int("tgid", call.Talkgroup).Msg("stored")
return nil return nil
} }
@ -42,6 +42,7 @@ func (s *DatabaseSink) SinkType() string {
func (s *DatabaseSink) toAddCallParams(call *calls.Call) database.AddCallParams { func (s *DatabaseSink) toAddCallParams(call *calls.Call) database.AddCallParams {
return database.AddCallParams{ return database.AddCallParams{
ID: call.ID,
Submitter: call.Submitter.Int32Ptr(), Submitter: call.Submitter.Int32Ptr(),
System: call.System, System: call.System,
Talkgroup: call.Talkgroup, Talkgroup: call.Talkgroup,

View file

@ -15,11 +15,17 @@ import (
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
type tgMap map[ID]Talkgroup type tgMap map[ID]*Talkgroup
type Store interface { type Store interface {
// TG retrieves a Talkgroup from the Store. // TG retrieves a Talkgroup from the Store.
TG(ctx context.Context, tg ID) (Talkgroup, error) TG(ctx context.Context, tg ID) (*Talkgroup, error)
// TGs retrieves many talkgroups from the Store.
TGs(ctx context.Context, tgs IDs) ([]*Talkgroup, error)
// SystemTGs retrieves all Talkgroups associated with a System.
SystemTGs(ctx context.Context, systemID int32) ([]*Talkgroup, error)
// SystemName retrieves a system name from the store. It returns the record and whether one was found. // SystemName retrieves a system name from the store. It returns the record and whether one was found.
SystemName(ctx context.Context, id int) (string, bool) SystemName(ctx context.Context, id int) (string, bool)
@ -117,7 +123,7 @@ func (t *cache) Hint(ctx context.Context, tgs []ID) error {
return nil return nil
} }
func (t *cache) add(rec Talkgroup) error { func (t *cache) add(rec *Talkgroup) error {
t.Lock() t.Lock()
defer t.Unlock() defer t.Unlock()
@ -128,16 +134,70 @@ func (t *cache) add(rec Talkgroup) error {
return t.AlertConfig.UnmarshalTGRules(tg, rec.Talkgroup.AlertConfig) return t.AlertConfig.UnmarshalTGRules(tg, rec.Talkgroup.AlertConfig)
} }
func rowToTalkgroup(r database.GetTalkgroupWithLearnedByPackedIDsRow) Talkgroup { type row interface {
return Talkgroup{ database.GetTalkgroupsWithLearnedByPackedIDsRow | database.GetTalkgroupsWithLearnedRow |
Talkgroup: r.Talkgroup, database.GetTalkgroupsWithLearnedBySystemRow
System: r.System, GetTalkgroup() database.Talkgroup
Learned: r.Learned, GetSystem() database.System
GetLearned() bool
}
func rowToTalkgroup[T row](r T) *Talkgroup {
return &Talkgroup{
Talkgroup: r.GetTalkgroup(),
System: r.GetSystem(),
Learned: r.GetLearned(),
} }
} }
func addToRowList[T row](t *cache, r []*Talkgroup, tgRecords []T) ([]*Talkgroup, error) {
for _, rec := range tgRecords {
tg := rowToTalkgroup(rec)
err := t.add(tg)
if err != nil {
return nil, err
}
r = append(r, tg)
}
return r, nil
}
func (t *cache) TGs(ctx context.Context, tgs IDs) ([]*Talkgroup, error) {
r := make([]*Talkgroup, 0, len(tgs))
var err error
if tgs != nil {
toGet := make(IDs, 0, len(tgs))
t.RLock()
for _, id := range tgs {
rec, has := t.tgs[id]
if has {
r = append(r, rec)
} else {
toGet = append(toGet, id)
}
}
t.RUnlock()
tgRecords, err := database.FromCtx(ctx).GetTalkgroupsWithLearnedByPackedIDs(ctx, toGet.Packed())
if err != nil {
return nil, err
}
return addToRowList(t, r, tgRecords)
}
// get all talkgroups
tgRecords, err := database.FromCtx(ctx).GetTalkgroupsWithLearned(ctx)
if err != nil {
return nil, err
}
return addToRowList(t, r, tgRecords)
}
func (t *cache) Load(ctx context.Context, tgs []int64) error { func (t *cache) Load(ctx context.Context, tgs []int64) error {
tgRecords, err := database.FromCtx(ctx).GetTalkgroupWithLearnedByPackedIDs(ctx, tgs) tgRecords, err := database.FromCtx(ctx).GetTalkgroupsWithLearnedByPackedIDs(ctx, tgs)
if err != nil { if err != nil {
return err return err
} }
@ -168,7 +228,17 @@ func (t *cache) Weight(ctx context.Context, id ID, tm time.Time) float64 {
return float64(m) return float64(m)
} }
func (t *cache) TG(ctx context.Context, tg ID) (Talkgroup, error) { func (t *cache) SystemTGs(ctx context.Context, systemID int32) ([]*Talkgroup, error) {
recs, err := database.FromCtx(ctx).GetTalkgroupsWithLearnedBySystem(ctx, systemID)
if err != nil {
return nil, err
}
r := make([]*Talkgroup, 0, len(recs))
return addToRowList(t, r, recs)
}
func (t *cache) TG(ctx context.Context, tg ID) (*Talkgroup, error) {
t.RLock() t.RLock()
rec, has := t.tgs[tg] rec, has := t.tgs[tg]
t.RUnlock() t.RUnlock()
@ -177,18 +247,18 @@ func (t *cache) TG(ctx context.Context, tg ID) (Talkgroup, error) {
return rec, nil return rec, nil
} }
recs, err := database.FromCtx(ctx).GetTalkgroupWithLearnedByPackedIDs(ctx, []int64{tg.Pack()}) recs, err := database.FromCtx(ctx).GetTalkgroupsWithLearnedByPackedIDs(ctx, []int64{tg.Pack()})
switch err { switch err {
case nil: case nil:
case pgx.ErrNoRows: case pgx.ErrNoRows:
return Talkgroup{}, ErrNotFound return nil, ErrNotFound
default: default:
log.Error().Err(err).Msg("TG() cache add db get") log.Error().Err(err).Msg("TG() cache add db get")
return Talkgroup{}, errors.Join(ErrNotFound, err) return nil, errors.Join(ErrNotFound, err)
} }
if len(recs) < 1 { if len(recs) < 1 {
return Talkgroup{}, ErrNotFound return nil, ErrNotFound
} }
err = t.add(rowToTalkgroup(recs[0])) err = t.add(rowToTalkgroup(recs[0]))

View file

@ -13,8 +13,19 @@ type Talkgroup struct {
} }
type ID struct { type ID struct {
System uint32 System uint32 `json:"sys"`
Talkgroup uint32 Talkgroup uint32 `json:"tg"`
}
type IDs []ID
func (ids *IDs) Packed() []int64 {
r := make([]int64, len(*ids))
for i := range *ids {
r[i] = (*ids)[i].Pack()
}
return r
} }
func TG[T int | uint | int64 | uint64 | int32 | uint32](sys, tgid T) ID { func TG[T int | uint | int64 | uint64 | int32 | uint32](sys, tgid T) ID {

View file

@ -1,4 +1,4 @@
-- name: AddCall :one -- name: AddCall :exec
INSERT INTO calls ( INSERT INTO calls (
id, id,
submitter, submitter,
@ -17,8 +17,25 @@ INSERT INTO calls (
tg_alpha_tag, tg_alpha_tag,
tg_group, tg_group,
source source
) VALUES (gen_random_uuid(), $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16) ) VALUES (
RETURNING id; @id,
@submitter,
@system,
@talkgroup,
@call_date,
@audio_name,
@audio_blob,
@audio_type,
@audio_url,
@duration,
@frequency,
@frequencies,
@patches,
@tg_label,
@tg_alpha_tag,
@tg_group,
@source
);
-- name: SetCallTranscript :exec -- name: SetCallTranscript :exec
UPDATE calls SET transcript = $2 WHERE id = $1; UPDATE calls SET transcript = $2 WHERE id = $1;

View file

@ -51,7 +51,7 @@ FROM talkgroups_learned tgl
JOIN systems sys ON tgl.system_id = sys.id JOIN systems sys ON tgl.system_id = sys.id
WHERE tgl.system_id = sqlc.arg(system_id) AND tgl.tgid = sqlc.arg(tgid) AND ignored IS NOT TRUE; WHERE tgl.system_id = sqlc.arg(system_id) AND tgl.tgid = sqlc.arg(tgid) AND ignored IS NOT TRUE;
-- name: GetTalkgroupWithLearnedByPackedIDs :many -- name: GetTalkgroupsWithLearnedByPackedIDs :many
SELECT SELECT
sqlc.embed(tg), sqlc.embed(sys), sqlc.embed(tg), sqlc.embed(sys),
FALSE learned FALSE learned
@ -69,5 +69,41 @@ FROM talkgroups_learned tgl
JOIN systems sys ON tgl.system_id = sys.id JOIN systems sys ON tgl.system_id = sys.id
WHERE systg2id(tgl.system_id, tgl.tgid) = ANY($1::INT8[]) AND ignored IS NOT TRUE; WHERE systg2id(tgl.system_id, tgl.tgid) = ANY($1::INT8[]) AND ignored IS NOT TRUE;
-- name: GetTalkgroupsWithLearnedBySystem :many
SELECT
sqlc.embed(tg), sqlc.embed(sys),
FALSE learned
FROM talkgroups tg
JOIN systems sys ON tg.system_id = sys.id
WHERE tg.system_id = @system
UNION
SELECT
tgl.id::INT8, tgl.system_id::INT4, tgl.tgid::INT4, tgl.name,
tgl.alpha_tag, tgl.alpha_tag, NULL::INTEGER, NULL::JSONB,
CASE WHEN tgl.alpha_tag IS NULL THEN NULL ELSE ARRAY[tgl.alpha_tag] END,
TRUE, NULL::JSONB, 1.0, sys.id, sys.name,
TRUE learned
FROM talkgroups_learned tgl
JOIN systems sys ON tgl.system_id = sys.id
WHERE tgl.system_id = @system AND ignored IS NOT TRUE;
-- name: GetTalkgroupsWithLearned :many
SELECT
sqlc.embed(tg), sqlc.embed(sys),
FALSE learned
FROM talkgroups tg
JOIN systems sys ON tg.system_id = sys.id
UNION
SELECT
tgl.id::INT8, tgl.system_id::INT4, tgl.tgid::INT4, tgl.name,
tgl.alpha_tag, tgl.alpha_tag, NULL::INTEGER, NULL::JSONB,
CASE WHEN tgl.alpha_tag IS NULL THEN NULL ELSE ARRAY[tgl.alpha_tag] END,
TRUE, NULL::JSONB, 1.0, sys.id, sys.name,
TRUE learned
FROM talkgroups_learned tgl
JOIN systems sys ON tgl.system_id = sys.id
WHERE ignored IS NOT TRUE;
-- name: GetSystemName :one -- name: GetSystemName :one
SELECT name FROM systems WHERE id = sqlc.arg(system_id); SELECT name FROM systems WHERE id = sqlc.arg(system_id);