calls trigger
This commit is contained in:
parent
2c298bef33
commit
a25aaccbf3
9 changed files with 120 additions and 47 deletions
|
@ -25,3 +25,16 @@ func RunE(c cmdOptions) func(cmd *cobra.Command, args []string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func PtrTo[T any](t T) *T {
|
||||||
|
return &t
|
||||||
|
}
|
||||||
|
|
||||||
|
func PtrOrNull[T comparable](val T) *T {
|
||||||
|
var zero T
|
||||||
|
if val == zero {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &val
|
||||||
|
}
|
||||||
|
|
|
@ -8,7 +8,6 @@ import (
|
||||||
|
|
||||||
"dynatron.me/x/stillbox/pkg/gordio/config"
|
"dynatron.me/x/stillbox/pkg/gordio/config"
|
||||||
"dynatron.me/x/stillbox/pkg/gordio/database"
|
"dynatron.me/x/stillbox/pkg/gordio/database"
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
"golang.org/x/term"
|
"golang.org/x/term"
|
||||||
|
@ -55,7 +54,7 @@ func AddUser(ctx context.Context, username, email string, isAdmin bool) error {
|
||||||
Username: username,
|
Username: username,
|
||||||
Password: string(hashpw),
|
Password: string(hashpw),
|
||||||
Email: email,
|
Email: email,
|
||||||
IsAdmin: pgtype.Bool{Bool: isAdmin, Valid: true},
|
IsAdmin: isAdmin,
|
||||||
})
|
})
|
||||||
|
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -27,9 +27,11 @@ INSERT INTO calls (
|
||||||
frequencies,
|
frequencies,
|
||||||
patches,
|
patches,
|
||||||
tg_label,
|
tg_label,
|
||||||
|
tg_tag,
|
||||||
|
tg_group,
|
||||||
source
|
source
|
||||||
) VALUES (gen_random_uuid(), $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
|
) VALUES (gen_random_uuid(), $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
|
||||||
RETURNING id, submitter, system, talkgroup, call_date, audio_name, audio_blob, audio_type, audio_url, frequency, frequencies, patches, tg_label, source, transcript
|
RETURNING id, submitter, system, talkgroup, call_date, audio_name, audio_blob, audio_type, audio_url, frequency, frequencies, patches, tg_label, tg_tag, tg_group, source, transcript
|
||||||
`
|
`
|
||||||
|
|
||||||
type AddCallParams struct {
|
type AddCallParams struct {
|
||||||
|
@ -41,11 +43,13 @@ type AddCallParams struct {
|
||||||
AudioBlob []byte `json:"audio_blob"`
|
AudioBlob []byte `json:"audio_blob"`
|
||||||
AudioType *string `json:"audio_type"`
|
AudioType *string `json:"audio_type"`
|
||||||
AudioUrl *string `json:"audio_url"`
|
AudioUrl *string `json:"audio_url"`
|
||||||
Frequency *int32 `json:"frequency"`
|
Frequency int `json:"frequency"`
|
||||||
Frequencies []byte `json:"frequencies"`
|
Frequencies []int `json:"frequencies"`
|
||||||
Patches []byte `json:"patches"`
|
Patches []int `json:"patches"`
|
||||||
TgLabel *string `json:"tg_label"`
|
TgLabel *string `json:"tg_label"`
|
||||||
Source *string `json:"source"`
|
TgTag *string `json:"tg_tag"`
|
||||||
|
TgGroup *string `json:"tg_group"`
|
||||||
|
Source int `json:"source"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) AddCall(ctx context.Context, arg AddCallParams) (Call, error) {
|
func (q *Queries) AddCall(ctx context.Context, arg AddCallParams) (Call, error) {
|
||||||
|
@ -62,6 +66,8 @@ func (q *Queries) AddCall(ctx context.Context, arg AddCallParams) (Call, error)
|
||||||
arg.Frequencies,
|
arg.Frequencies,
|
||||||
arg.Patches,
|
arg.Patches,
|
||||||
arg.TgLabel,
|
arg.TgLabel,
|
||||||
|
arg.TgTag,
|
||||||
|
arg.TgGroup,
|
||||||
arg.Source,
|
arg.Source,
|
||||||
)
|
)
|
||||||
var i Call
|
var i Call
|
||||||
|
@ -79,6 +85,8 @@ func (q *Queries) AddCall(ctx context.Context, arg AddCallParams) (Call, error)
|
||||||
&i.Frequencies,
|
&i.Frequencies,
|
||||||
&i.Patches,
|
&i.Patches,
|
||||||
&i.TgLabel,
|
&i.TgLabel,
|
||||||
|
&i.TgTag,
|
||||||
|
&i.TgGroup,
|
||||||
&i.Source,
|
&i.Source,
|
||||||
&i.Transcript,
|
&i.Transcript,
|
||||||
)
|
)
|
||||||
|
|
|
@ -13,7 +13,7 @@ import (
|
||||||
|
|
||||||
type ApiKey struct {
|
type ApiKey struct {
|
||||||
ID int32 `json:"id"`
|
ID int32 `json:"id"`
|
||||||
Owner *int32 `json:"owner"`
|
Owner int `json:"owner"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
Expires pgtype.Timestamp `json:"expires"`
|
Expires pgtype.Timestamp `json:"expires"`
|
||||||
Disabled *bool `json:"disabled"`
|
Disabled *bool `json:"disabled"`
|
||||||
|
@ -30,11 +30,13 @@ type Call struct {
|
||||||
AudioBlob []byte `json:"audio_blob"`
|
AudioBlob []byte `json:"audio_blob"`
|
||||||
AudioType *string `json:"audio_type"`
|
AudioType *string `json:"audio_type"`
|
||||||
AudioUrl *string `json:"audio_url"`
|
AudioUrl *string `json:"audio_url"`
|
||||||
Frequency *int32 `json:"frequency"`
|
Frequency int `json:"frequency"`
|
||||||
Frequencies []byte `json:"frequencies"`
|
Frequencies []int `json:"frequencies"`
|
||||||
Patches []byte `json:"patches"`
|
Patches []int `json:"patches"`
|
||||||
TgLabel *string `json:"tg_label"`
|
TgLabel *string `json:"tg_label"`
|
||||||
Source *string `json:"source"`
|
TgTag *string `json:"tg_tag"`
|
||||||
|
TgGroup *string `json:"tg_group"`
|
||||||
|
Source int `json:"source"`
|
||||||
Transcript *string `json:"transcript"`
|
Transcript *string `json:"transcript"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -74,6 +76,14 @@ type Talkgroup struct {
|
||||||
Metadata []byte `json:"metadata"`
|
Metadata []byte `json:"metadata"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TalkgroupsLearned struct {
|
||||||
|
ID int32 `json:"id"`
|
||||||
|
SystemID int `json:"system_id"`
|
||||||
|
Tgid int `json:"tgid"`
|
||||||
|
GroupName string `json:"group_name"`
|
||||||
|
GroupTag *string `json:"group_tag"`
|
||||||
|
}
|
||||||
|
|
||||||
type TalkgroupsTag struct {
|
type TalkgroupsTag struct {
|
||||||
SystemID int `json:"system_id"`
|
SystemID int `json:"system_id"`
|
||||||
TalkgroupID int `json:"talkgroup_id"`
|
TalkgroupID int `json:"talkgroup_id"`
|
||||||
|
@ -85,6 +95,6 @@ type User struct {
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
IsAdmin *bool `json:"is_admin"`
|
IsAdmin bool `json:"is_admin"`
|
||||||
Prefs []byte `json:"prefs"`
|
Prefs []byte `json:"prefs"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,7 +23,7 @@ INSERT INTO api_keys(
|
||||||
RETURNING id, owner, created_at, expires, disabled, api_key
|
RETURNING id, owner, created_at, expires, disabled, api_key
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) CreateAPIKey(ctx context.Context, owner *int32, expires pgtype.Timestamp, disabled *bool) (ApiKey, error) {
|
func (q *Queries) CreateAPIKey(ctx context.Context, owner int, expires pgtype.Timestamp, disabled *bool) (ApiKey, error) {
|
||||||
row := q.db.QueryRow(ctx, createAPIKey, owner, expires, disabled)
|
row := q.db.QueryRow(ctx, createAPIKey, owner, expires, disabled)
|
||||||
var i ApiKey
|
var i ApiKey
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
|
@ -51,7 +51,7 @@ type CreateUserParams struct {
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
IsAdmin *bool `json:"is_admin"`
|
IsAdmin bool `json:"is_admin"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, error) {
|
func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, error) {
|
||||||
|
|
|
@ -9,9 +9,9 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"dynatron.me/x/stillbox/internal/common"
|
||||||
"dynatron.me/x/stillbox/pkg/gordio/database"
|
"dynatron.me/x/stillbox/pkg/gordio/database"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/jackc/pgtype"
|
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -34,26 +34,22 @@ type callUploadRequest struct {
|
||||||
TalkgroupTag string `form:"talkgroupTag"`
|
TalkgroupTag string `form:"talkgroupTag"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AddCallParams struct {
|
|
||||||
Submitter pgtype.Int4 `json:"submitter"`
|
|
||||||
System int32 `json:"system"`
|
|
||||||
Talkgroup int32 `json:"talkgroup"`
|
|
||||||
CallDate pgtype.Timestamp `json:"call_date"`
|
|
||||||
AudioName pgtype.Text `json:"audio_name"`
|
|
||||||
AudioBlob []byte `json:"audio_blob"`
|
|
||||||
AudioType pgtype.Text `json:"audio_type"`
|
|
||||||
AudioUrl pgtype.Text `json:"audio_url"`
|
|
||||||
Frequency pgtype.Int4 `json:"frequency"`
|
|
||||||
Frequencies []byte `json:"frequencies"`
|
|
||||||
Patches []byte `json:"patches"`
|
|
||||||
TgLabel pgtype.Text `json:"tg_label"`
|
|
||||||
Source pgtype.Text `json:"source"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (car *callUploadRequest) ToAddCallParams(submitter int) database.AddCallParams {
|
func (car *callUploadRequest) ToAddCallParams(submitter int) database.AddCallParams {
|
||||||
return database.AddCallParams{
|
return database.AddCallParams{
|
||||||
Submitter: submitter,
|
Submitter: common.PtrTo(int32(submitter)),
|
||||||
System: car.System,
|
System: car.System,
|
||||||
|
Talkgroup: car.Talkgroup,
|
||||||
|
CallDate: car.DateTime,
|
||||||
|
AudioName: common.PtrOrNull(car.AudioName),
|
||||||
|
AudioBlob: car.Audio,
|
||||||
|
AudioType: common.PtrOrNull(car.AudioType),
|
||||||
|
Frequency: car.Frequency,
|
||||||
|
Frequencies: car.Frequencies,
|
||||||
|
Patches: car.Patches,
|
||||||
|
TgLabel: common.PtrOrNull(car.TalkgroupLabel),
|
||||||
|
TgTag: common.PtrOrNull(car.TalkgroupTag),
|
||||||
|
TgGroup: common.PtrOrNull(car.TalkgroupGroup),
|
||||||
|
Source: car.Source,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -81,7 +77,7 @@ func (s *Server) routeCallUpload(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if apik.Disabled.Bool || (apik.Expires.Valid && time.Now().After(apik.Expires.Time)) {
|
if (apik.Disabled != nil && *apik.Disabled) || (apik.Expires.Valid && time.Now().After(apik.Expires.Time)) {
|
||||||
http.Error(w, "disabled", http.StatusUnauthorized)
|
http.Error(w, "disabled", http.StatusUnauthorized)
|
||||||
log.Error().Str("key", apik.ApiKey.String()).Msg("key disabled")
|
log.Error().Str("key", apik.ApiKey.String()).Msg("key disabled")
|
||||||
return
|
return
|
||||||
|
@ -94,12 +90,14 @@ func (s *Server) routeCallUpload(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
dbCall, err := db.AddCall(r.Context(), call.ToAddCallParams())
|
dbCall, err := db.AddCall(r.Context(), call.ToAddCallParams(apik.Owner))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "internal error", http.StatusInternalServerError)
|
http.Error(w, "internal error", http.StatusInternalServerError)
|
||||||
log.Error().Err(err).Msg("add call")
|
log.Error().Err(err).Msg("add call")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_ = dbCall
|
||||||
}
|
}
|
||||||
|
|
||||||
func (car *callUploadRequest) fill(r *http.Request) error {
|
func (car *callUploadRequest) fill(r *http.Request) error {
|
||||||
|
@ -124,7 +122,12 @@ func (car *callUploadRequest) fill(r *http.Request) error {
|
||||||
|
|
||||||
f.SetBytes(audioBytes)
|
f.SetBytes(audioBytes)
|
||||||
case time.Time:
|
case time.Time:
|
||||||
t, err := time.Parse(time.RFC3339, r.Form.Get(formField))
|
tval := r.Form.Get(formField)
|
||||||
|
if iv, err := strconv.Atoi(tval); err == nil {
|
||||||
|
f.Set(reflect.ValueOf(time.Unix(int64(iv), 0)))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
t, err := time.Parse(time.RFC3339, tval)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("parse time: %w", err)
|
return fmt.Errorf("parse time: %w", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ CREATE TABLE IF NOT EXISTS users(
|
||||||
username VARCHAR (255) UNIQUE NOT NULL,
|
username VARCHAR (255) UNIQUE NOT NULL,
|
||||||
password TEXT NOT NULL,
|
password TEXT NOT NULL,
|
||||||
email TEXT NOT NULL,
|
email TEXT NOT NULL,
|
||||||
is_admin BOOLEAN,
|
is_admin BOOLEAN NOT NULL,
|
||||||
prefs JSONB
|
prefs JSONB
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ CREATE INDEX IF NOT EXISTS users_username_idx ON users(username);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS api_keys(
|
CREATE TABLE IF NOT EXISTS api_keys(
|
||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
owner INTEGER REFERENCES users(id),
|
owner INTEGER REFERENCES users(id) NOT NULL,
|
||||||
created_at TIMESTAMP NOT NULL,
|
created_at TIMESTAMP NOT NULL,
|
||||||
expires TIMESTAMP,
|
expires TIMESTAMP,
|
||||||
disabled BOOLEAN,
|
disabled BOOLEAN,
|
||||||
|
@ -33,6 +33,37 @@ CREATE TABLE IF NOT EXISTS talkgroups(
|
||||||
PRIMARY KEY (system_id, tgid)
|
PRIMARY KEY (system_id, tgid)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS talkgroups_learned(
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
system_id INTEGER REFERENCES systems(id) NOT NULL,
|
||||||
|
tgid INTEGER NOT NULL,
|
||||||
|
group_name TEXT NOT NULL,
|
||||||
|
group_tag TEXT,
|
||||||
|
UNIQUE (system_id, tgid, group_name, group_tag)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION learn_talkgroup()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (SELECT *
|
||||||
|
FROM talkgroups
|
||||||
|
JOIN talkgroups_tags
|
||||||
|
ON talkgroups_tags.system_id = talkgroups.system_id AND talkgroups_tags.talkgroup_id = talkgroups.tgid
|
||||||
|
WHERE
|
||||||
|
talkgroups.system_id = NEW.system AND talkgroups.tgid = NEW.talkgroup AND
|
||||||
|
(
|
||||||
|
talkgroups.name != NEW.tg_label
|
||||||
|
OR NOT (talkgroups_tags.tags @> ARRAY[NEW.tg_tag])
|
||||||
|
)
|
||||||
|
) THEN
|
||||||
|
INSERT INTO talkgroups_learned(system_id, tgid, group_name, group_tag) VALUES(
|
||||||
|
NEW.system, NEW.talkgroup, NEW.tg_label, NEW.tg_tag
|
||||||
|
) ON CONFLICT DO NOTHING;
|
||||||
|
END IF;
|
||||||
|
RETURN NEW;
|
||||||
|
END
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS talkgroups_tags(
|
CREATE TABLE IF NOT EXISTS talkgroups_tags(
|
||||||
system_id INTEGER NOT NULL,
|
system_id INTEGER NOT NULL,
|
||||||
talkgroup_id INTEGER NOT NULL,
|
talkgroup_id INTEGER NOT NULL,
|
||||||
|
@ -52,15 +83,20 @@ CREATE TABLE IF NOT EXISTS calls(
|
||||||
audio_blob BYTEA,
|
audio_blob BYTEA,
|
||||||
audio_type TEXT,
|
audio_type TEXT,
|
||||||
audio_url TEXT,
|
audio_url TEXT,
|
||||||
frequency INTEGER,
|
frequency INTEGER NOT NULL,
|
||||||
frequencies JSONB,
|
frequencies INTEGER[],
|
||||||
patches JSONB,
|
patches INTEGER[],
|
||||||
tg_label TEXT,
|
tg_label TEXT,
|
||||||
source TEXT,
|
tg_tag TEXT,
|
||||||
|
tg_group TEXT,
|
||||||
|
source INTEGER NOT NULL,
|
||||||
transcript TEXT
|
transcript TEXT
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
|
CREATE OR REPLACE TRIGGER learn_tg AFTER INSERT ON calls
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION learn_talkgroup();
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS calls_transcript_idx ON calls USING GIN (to_tsvector('english', transcript));
|
CREATE INDEX IF NOT EXISTS calls_transcript_idx ON calls USING GIN (to_tsvector('english', transcript));
|
||||||
CREATE INDEX IF NOT EXISTS calls_call_date_tg_idx ON calls(system, talkgroup, call_date);
|
CREATE INDEX IF NOT EXISTS calls_call_date_tg_idx ON calls(system, talkgroup, call_date);
|
||||||
|
|
||||||
|
@ -89,6 +125,5 @@ CREATE TABLE IF NOT EXISTS incidents_calls(
|
||||||
incident_id UUID REFERENCES incidents(id) ON UPDATE CASCADE ON DELETE CASCADE,
|
incident_id UUID REFERENCES incidents(id) ON UPDATE CASCADE ON DELETE CASCADE,
|
||||||
call_id UUID REFERENCES calls(id) ON UPDATE CASCADE,
|
call_id UUID REFERENCES calls(id) ON UPDATE CASCADE,
|
||||||
notes JSONB,
|
notes JSONB,
|
||||||
-- CONSTRAINT incident_call_pkey PRIMARY KEY (incident_id, call_id)
|
|
||||||
PRIMARY KEY (incident_id, call_id)
|
PRIMARY KEY (incident_id, call_id)
|
||||||
);
|
);
|
||||||
|
|
|
@ -13,8 +13,10 @@ INSERT INTO calls (
|
||||||
frequencies,
|
frequencies,
|
||||||
patches,
|
patches,
|
||||||
tg_label,
|
tg_label,
|
||||||
|
tg_tag,
|
||||||
|
tg_group,
|
||||||
source
|
source
|
||||||
) VALUES (gen_random_uuid(), $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
|
) VALUES (gen_random_uuid(), $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
|
||||||
RETURNING *;
|
RETURNING *;
|
||||||
|
|
||||||
-- name: SetCallTranscript :exec
|
-- name: SetCallTranscript :exec
|
||||||
|
|
|
@ -10,6 +10,7 @@ sql:
|
||||||
sql_package: "pgx/v5"
|
sql_package: "pgx/v5"
|
||||||
query_parameter_limit: 3
|
query_parameter_limit: 3
|
||||||
emit_json_tags: true
|
emit_json_tags: true
|
||||||
|
emit_interface: true
|
||||||
emit_pointers_for_null_types: true
|
emit_pointers_for_null_types: true
|
||||||
overrides:
|
overrides:
|
||||||
- db_type: "uuid"
|
- db_type: "uuid"
|
||||||
|
@ -20,7 +21,9 @@ sql:
|
||||||
go_type: "int"
|
go_type: "int"
|
||||||
- db_type: "pg_catalog.serial4"
|
- db_type: "pg_catalog.serial4"
|
||||||
go_type: "int"
|
go_type: "int"
|
||||||
- db_type: "int"
|
- db_type: "integer"
|
||||||
go_type: "int"
|
go_type: "int"
|
||||||
- db_type: "pg_catalog.timestamp"
|
- db_type: "pg_catalog.timestamp"
|
||||||
go_type: "time.Time"
|
go_type: "time.Time"
|
||||||
|
- db_type: "pg_catalog.text"
|
||||||
|
go_type: "string"
|
||||||
|
|
Loading…
Reference in a new issue