calls trigger

This commit is contained in:
Daniel Ponte 2024-07-27 19:25:16 -04:00
parent 2c298bef33
commit a25aaccbf3
9 changed files with 120 additions and 47 deletions

View file

@ -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
}

View file

@ -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

View file

@ -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,
) )

View file

@ -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"`
} }

View file

@ -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) {

View file

@ -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)
} }

View file

@ -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)
); );

View file

@ -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

View file

@ -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"