mirror of
https://github.com/chuot/rdio-scanner.git
synced 2026-08-13 08:33:00 -06:00
- Introduced a new document, API_ACCESS_POLICY.md, outlining the terms of use for the WebSocket API, including license, scope of restriction, authorized access, authentication, prohibited use, enforcement, and contact information. - Updated README.md to highlight the restricted nature of the WebSocket API and link to the new access policy. - Changed copyright information in multiple server files to reflect the new email address for Chrystian Huot. - Added comments in server files to indicate the WebSocket API access policy and its restrictions.
680 lines
19 KiB
Go
680 lines
19 KiB
Go
// Copyright (C) 2019-2026 Chrystian Huot <chrystian.huot@saubeo.solutions>
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU General Public License
|
|
// along with this program. If not, see <http://www.gnu.org/licenses/>
|
|
//
|
|
// WebSocket API Access Policy:
|
|
// This WebSocket API is reserved exclusively for Saubeo Solutions and its native applications.
|
|
// Unauthorized access is strictly prohibited.
|
|
// See API_ACCESS_POLICY.md for full terms.
|
|
|
|
package main
|
|
|
|
import (
|
|
"database/sql"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"math"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
type CallFrequency struct {
|
|
Id uint64
|
|
CallId uint64
|
|
Dbm int
|
|
Errors uint
|
|
Frequency uint
|
|
Offset float32
|
|
Spikes uint
|
|
}
|
|
|
|
type CallMeta struct {
|
|
SiteId uint64
|
|
SiteLabel string
|
|
SiteRef uint
|
|
SystemId uint64
|
|
SystemLabel string
|
|
SystemRef uint
|
|
TalkgroupGroups []string
|
|
TalkgroupId uint64
|
|
TalkgroupLabel string
|
|
TalkgroupName string
|
|
TalkgroupRef uint
|
|
TalkgroupTag string
|
|
UnitLabels []string
|
|
UnitRefs []uint
|
|
}
|
|
|
|
type CallUnit struct {
|
|
Id uint64
|
|
CallId uint64
|
|
Offset float32
|
|
UnitRef uint
|
|
}
|
|
|
|
type Call struct {
|
|
Id uint64
|
|
Audio []byte
|
|
AudioFilename string
|
|
AudioMime string
|
|
Delayed bool
|
|
Frequencies []CallFrequency
|
|
Meta CallMeta
|
|
Patches []uint
|
|
SiteRef uint
|
|
System *System
|
|
Talkgroup *Talkgroup
|
|
Timestamp time.Time
|
|
Units []CallUnit
|
|
}
|
|
|
|
func NewCall() *Call {
|
|
return &Call{
|
|
Frequencies: []CallFrequency{},
|
|
Meta: CallMeta{
|
|
TalkgroupGroups: []string{},
|
|
UnitLabels: []string{},
|
|
UnitRefs: []uint{},
|
|
},
|
|
Patches: []uint{},
|
|
Units: []CallUnit{},
|
|
}
|
|
}
|
|
|
|
func (call *Call) IsValid() (ok bool, err error) {
|
|
ok = true
|
|
|
|
if len(call.Audio) <= 44 {
|
|
ok = false
|
|
err = errors.New("no audio")
|
|
|
|
} else if call.Timestamp.UnixMilli() == 0 {
|
|
ok = false
|
|
err = errors.New("no timestamp")
|
|
|
|
} else if !(call.System != nil || call.Meta.SystemId > 0 || len(call.Meta.SystemLabel) > 0 || call.Meta.SystemRef > 0) {
|
|
ok = false
|
|
err = errors.New("no system")
|
|
|
|
} else if !(call.Talkgroup != nil || call.Meta.TalkgroupId > 0 || len(call.Meta.TalkgroupLabel) > 0 || call.Meta.TalkgroupRef > 0) {
|
|
ok = false
|
|
err = errors.New("no talkgroup")
|
|
}
|
|
|
|
return ok, err
|
|
}
|
|
|
|
func (call *Call) MarshalJSON() ([]byte, error) {
|
|
audio := strings.ReplaceAll(fmt.Sprintf("%v", call.Audio), " ", ",")
|
|
|
|
callMap := map[string]any{
|
|
"id": call.Id,
|
|
"audio": map[string]any{
|
|
"data": json.RawMessage(audio),
|
|
"type": "Buffer",
|
|
},
|
|
"audioName": call.AudioFilename,
|
|
"audioType": call.AudioMime,
|
|
"dateTime": call.Timestamp.Format(time.RFC3339),
|
|
"delayed": call.Delayed,
|
|
"patches": call.Patches,
|
|
}
|
|
|
|
if len(call.Frequencies) > 0 {
|
|
freqs := []map[string]any{}
|
|
for _, f := range call.Frequencies {
|
|
freq := map[string]any{
|
|
"errorCount": f.Errors,
|
|
"freq": f.Frequency,
|
|
"pos": f.Offset,
|
|
"spikeCount": f.Spikes,
|
|
}
|
|
|
|
if f.Dbm > 0 {
|
|
freq["dbm"] = f.Dbm
|
|
}
|
|
|
|
freqs = append(freqs, freq)
|
|
}
|
|
|
|
callMap["frequencies"] = freqs
|
|
}
|
|
|
|
if call.SiteRef > 0 {
|
|
callMap["site"] = call.SiteRef
|
|
}
|
|
|
|
if call.System != nil {
|
|
callMap["system"] = call.System.SystemRef
|
|
}
|
|
|
|
if call.Talkgroup != nil {
|
|
callMap["talkgroup"] = call.Talkgroup.TalkgroupRef
|
|
}
|
|
|
|
if len(call.Units) > 0 {
|
|
sources := []map[string]any{}
|
|
for _, unit := range call.Units {
|
|
sources = append(sources, map[string]any{
|
|
"pos": unit.Offset,
|
|
"src": unit.UnitRef,
|
|
})
|
|
}
|
|
callMap["sources"] = sources
|
|
}
|
|
|
|
return json.Marshal(callMap)
|
|
}
|
|
|
|
func (call *Call) ToJson() (string, error) {
|
|
if b, err := json.Marshal(call); err == nil {
|
|
return string(b), nil
|
|
} else {
|
|
return "", fmt.Errorf("call.tojson: %v", err)
|
|
}
|
|
}
|
|
|
|
type Calls struct {
|
|
controller *Controller
|
|
mutex sync.Mutex
|
|
}
|
|
|
|
func NewCalls(controller *Controller) *Calls {
|
|
return &Calls{
|
|
controller: controller,
|
|
mutex: sync.Mutex{},
|
|
}
|
|
}
|
|
|
|
func (calls *Calls) CheckDuplicate(call *Call, msTimeFrame uint, db *Database) (bool, error) {
|
|
var count uint64
|
|
|
|
calls.mutex.Lock()
|
|
defer calls.mutex.Unlock()
|
|
|
|
formatError := errorFormatter("calls", "checkduplicate")
|
|
|
|
d := time.Duration(msTimeFrame) * time.Millisecond
|
|
from := call.Timestamp.Add(-d)
|
|
to := call.Timestamp.Add(d)
|
|
|
|
query := fmt.Sprintf(`SELECT COUNT(*) FROM "calls" WHERE ("timestamp" BETWEEN %d and %d) AND "systemId" = %d AND "talkgroupId" = %d`, from.UnixMilli(), to.UnixMilli(), call.System.Id, call.Talkgroup.Id)
|
|
if err := db.Sql.QueryRow(query).Scan(&count); err != nil {
|
|
return false, formatError(err, query)
|
|
}
|
|
|
|
return count > 0, nil
|
|
}
|
|
|
|
func (calls *Calls) GetCall(id uint64) (*Call, error) {
|
|
var (
|
|
err error
|
|
query string
|
|
rows *sql.Rows
|
|
tx *sql.Tx
|
|
|
|
patch string
|
|
systemId uint64
|
|
talkgroupId uint64
|
|
timestamp int64
|
|
)
|
|
|
|
calls.mutex.Lock()
|
|
defer calls.mutex.Unlock()
|
|
|
|
formatError := errorFormatter("calls", "getcall")
|
|
|
|
if tx, err = calls.controller.Database.Sql.Begin(); err != nil {
|
|
return nil, formatError(err, "")
|
|
}
|
|
|
|
call := Call{Id: id}
|
|
|
|
if calls.controller.Database.Config.DbType == DbTypePostgresql {
|
|
query = fmt.Sprintf(`SELECT c."audio", c."audioFilename", c."audioMime", c."siteRef", c."timestamp", STRING_AGG(CAST(COALESCE(cpt."talkgroupRef", 0) AS text), ','), c."siteRef", sy."systemId", t."talkgroupId" FROM "calls" AS c LEFT JOIN "callPatches" AS cp on cp."callId" = c."callId" LEFT JOIN "talkgroups" AS cpt ON cpt."talkgroupId" = cp."talkgroupId" LEFT JOIN "systems" AS sy ON sy."systemId" = c."systemId" LEFT JOIN "talkgroups" AS t ON t."talkgroupId" = c."talkgroupId" WHERE c."callId" = %d GROUP BY c."callId"`, id)
|
|
|
|
} else {
|
|
query = fmt.Sprintf(`SELECT c."audio", c."audioFilename", c."audioMime", c."siteRef", c."timestamp", GROUP_CONCAT(COALESCE(cpt."talkgroupRef", 0)), c."siteRef", sy."systemId", t."talkgroupId" FROM "calls" AS c LEFT JOIN "callPatches" AS cp on cp."callId" = c."callId" LEFT JOIN "talkgroups" AS cpt ON cpt."talkgroupId" = cp."talkgroupId" LEFT JOIN "systems" AS sy ON sy."systemId" = c."systemId" LEFT JOIN "talkgroups" AS t ON t."talkgroupId" = c."talkgroupId" WHERE c."callId" = %d GROUP BY c."callId"`, id)
|
|
}
|
|
|
|
if err = tx.QueryRow(query).Scan(&call.Audio, &call.AudioFilename, &call.AudioMime, &patch, ×tamp, &patch, &call.SiteRef, &systemId, &talkgroupId); err != nil && err != sql.ErrNoRows {
|
|
tx.Rollback()
|
|
return nil, formatError(err, query)
|
|
}
|
|
|
|
call.Timestamp = time.UnixMilli(timestamp)
|
|
|
|
if len(patch) > 0 {
|
|
for _, s := range strings.Split(patch, ",") {
|
|
if i, err := strconv.Atoi(s); err == nil && i > 0 {
|
|
call.Patches = append(call.Patches, uint(i))
|
|
}
|
|
}
|
|
}
|
|
|
|
if system, ok := calls.controller.Systems.GetSystemById(systemId); ok {
|
|
call.System = system
|
|
|
|
} else {
|
|
return nil, formatError(fmt.Errorf("cannot retrieve system id %d for call id %d", systemId, call.Id), "")
|
|
}
|
|
|
|
if talkgroup, ok := call.System.Talkgroups.GetTalkgroupById(talkgroupId); ok {
|
|
call.Talkgroup = talkgroup
|
|
|
|
} else {
|
|
return nil, formatError(fmt.Errorf("cannot retrieve talkgroup id %d for call id %d", talkgroupId, call.Id), "")
|
|
}
|
|
|
|
query = fmt.Sprintf(`SELECT "dbm", "errors", "frequency", "offset", "spikes" FROM "callFrequencies" WHERE "callId" = %d`, id)
|
|
if rows, err = tx.Query(query); err != nil {
|
|
tx.Rollback()
|
|
return nil, formatError(err, query)
|
|
}
|
|
|
|
for rows.Next() {
|
|
f := CallFrequency{}
|
|
|
|
if err = rows.Scan(&f.Dbm, &f.Errors, &f.Frequency, &f.Offset, &f.Spikes); err != nil {
|
|
break
|
|
}
|
|
|
|
call.Frequencies = append(call.Frequencies, f)
|
|
}
|
|
|
|
rows.Close()
|
|
|
|
if err != nil {
|
|
tx.Rollback()
|
|
return nil, formatError(err, query)
|
|
}
|
|
|
|
query = fmt.Sprintf(`SELECT "offset", "unitRef" FROM "callUnits" WHERE "callId" = %d`, id)
|
|
if rows, err = tx.Query(query); err != nil {
|
|
tx.Rollback()
|
|
return nil, formatError(err, query)
|
|
}
|
|
|
|
for rows.Next() {
|
|
u := CallUnit{}
|
|
|
|
if err = rows.Scan(&u.Offset, &u.UnitRef); err != nil {
|
|
break
|
|
}
|
|
|
|
call.Units = append(call.Units, u)
|
|
}
|
|
|
|
rows.Close()
|
|
|
|
if err = tx.Commit(); err != nil {
|
|
tx.Rollback()
|
|
return nil, formatError(err, "")
|
|
}
|
|
|
|
return &call, nil
|
|
}
|
|
|
|
func (calls *Calls) Prune(db *Database, pruneDays uint) error {
|
|
calls.mutex.Lock()
|
|
defer calls.mutex.Unlock()
|
|
|
|
timestamp := time.Now().Add(-24 * time.Hour * time.Duration(pruneDays)).UnixMilli()
|
|
query := fmt.Sprintf(`DELETE FROM "calls" WHERE "timestamp" < %d`, timestamp)
|
|
|
|
if _, err := db.Sql.Exec(query); err != nil {
|
|
return fmt.Errorf("%s in %s", err, query)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (calls *Calls) Search(searchOptions *CallsSearchOptions, client *Client) (*CallsSearchResults, error) {
|
|
const (
|
|
ascOrder = "ASC"
|
|
descOrder = "DESC"
|
|
)
|
|
|
|
var (
|
|
err error
|
|
rows *sql.Rows
|
|
|
|
limit uint
|
|
offset uint
|
|
order string
|
|
query string
|
|
where string = `c."systemId" > 0 AND c."talkgroupId" > 0 AND s."systemRef" IS NOT NULL AND t."talkgroupRef" IS NOT NULL AND d."callId" IS NULL`
|
|
|
|
timestamp int64
|
|
)
|
|
|
|
calls.mutex.Lock()
|
|
defer calls.mutex.Unlock()
|
|
|
|
db := client.Controller.Database
|
|
|
|
formatError := errorFormatter("calls", "search")
|
|
|
|
searchResults := &CallsSearchResults{
|
|
Options: searchOptions,
|
|
Results: []CallsSearchResult{},
|
|
}
|
|
|
|
if client.Access != nil {
|
|
switch v := client.Access.Systems.(type) {
|
|
case []any:
|
|
a := []string{}
|
|
for _, scope := range v {
|
|
var c string
|
|
switch v := scope.(type) {
|
|
case map[string]any:
|
|
switch v["talkgroups"].(type) {
|
|
case []any:
|
|
b := strings.ReplaceAll(fmt.Sprintf("%v", v["talkgroups"]), " ", ", ")
|
|
b = strings.ReplaceAll(b, "[", "(")
|
|
b = strings.ReplaceAll(b, "]", ")")
|
|
c = fmt.Sprintf(`(s."systemRef" = %d AND t."talkgroupRef" IN %v)`, v["id"], b)
|
|
case string:
|
|
if v["talkgroups"] == "*" {
|
|
c = fmt.Sprintf(`s."systemRef" = %d`, v["id"])
|
|
}
|
|
}
|
|
}
|
|
if len(c) > 0 {
|
|
a = append(a, c)
|
|
}
|
|
}
|
|
where = fmt.Sprintf("(%s)", strings.Join(a, " OR "))
|
|
}
|
|
}
|
|
|
|
switch v := searchOptions.System.(type) {
|
|
case uint:
|
|
a := []string{
|
|
fmt.Sprintf(`s."systemRef" = %d`, v),
|
|
}
|
|
switch v := searchOptions.Talkgroup.(type) {
|
|
case uint:
|
|
a = append(a, fmt.Sprintf(`t."talkgroupRef" = %d`, v))
|
|
}
|
|
where += fmt.Sprintf(" AND (%s)", strings.Join(a, " AND "))
|
|
}
|
|
|
|
switch v := searchOptions.Group.(type) {
|
|
case string:
|
|
a := []string{}
|
|
for id, m := range client.GroupsMap[v] {
|
|
in := strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(fmt.Sprintf("%v", m), " ", ", "), "[", "("), "]", ")")
|
|
a = append(a, fmt.Sprintf(`(s."systemRef" = %d AND t."talkgroupRef" IN %s)`, id, in))
|
|
}
|
|
if len(a) > 0 {
|
|
where += fmt.Sprintf(" AND (%s)", strings.Join(a, " OR "))
|
|
}
|
|
}
|
|
|
|
switch v := searchOptions.Tag.(type) {
|
|
case string:
|
|
a := []string{}
|
|
for id, m := range client.TagsMap[v] {
|
|
in := strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(fmt.Sprintf("%v", m), " ", ", "), "[", "("), "]", ")")
|
|
a = append(a, fmt.Sprintf(`(s."systemRef" = %d AND t."talkgroupRef" IN %s)`, id, in))
|
|
}
|
|
if len(a) > 0 {
|
|
where += fmt.Sprintf(" AND (%s)", strings.Join(a, " OR "))
|
|
}
|
|
}
|
|
|
|
query = fmt.Sprintf(`SELECT c."timestamp" FROM "calls" AS c LEFT JOIN "systems" AS s ON s."systemId" = c."systemId" LEFT JOIN "talkgroups" AS t ON t."talkgroupId" = c."talkgroupId" LEFT JOIN "delayed" AS d ON d."callId" = c."callId" WHERE %s ORDER BY c."timestamp" ASC`, where)
|
|
if err = db.Sql.QueryRow(query).Scan(×tamp); err != nil && err != sql.ErrNoRows {
|
|
return nil, formatError(err, query)
|
|
}
|
|
|
|
searchResults.DateStart = time.UnixMilli(timestamp)
|
|
|
|
query = fmt.Sprintf(`SELECT c."timestamp" FROM "calls" AS c LEFT JOIN "systems" AS s ON s."systemId" = c."systemId" LEFT JOIN "talkgroups" AS t ON t."talkgroupId" = c."talkgroupId" LEFT JOIN "delayed" AS d ON d."callId" = c."callId" WHERE %s ORDER BY c."timestamp" DESC`, where)
|
|
if err = db.Sql.QueryRow(query).Scan(×tamp); err != nil && err != sql.ErrNoRows {
|
|
return nil, formatError(err, query)
|
|
}
|
|
|
|
searchResults.DateStop = time.UnixMilli(timestamp)
|
|
|
|
switch v := searchOptions.Sort.(type) {
|
|
case int:
|
|
if v < 0 {
|
|
order = descOrder
|
|
} else {
|
|
order = ascOrder
|
|
}
|
|
default:
|
|
order = ascOrder
|
|
}
|
|
|
|
switch v := searchOptions.Date.(type) {
|
|
case time.Time:
|
|
var (
|
|
start time.Time
|
|
stop time.Time
|
|
)
|
|
|
|
if order == ascOrder {
|
|
start = time.Date(v.Year(), v.Month(), v.Day(), v.Hour(), v.Minute(), 0, 0, time.UTC)
|
|
stop = start.Add(time.Hour*24 - time.Millisecond)
|
|
|
|
} else {
|
|
start = time.Date(v.Year(), v.Month(), v.Day(), v.Hour(), v.Minute(), 0, 0, time.UTC).Add(time.Hour*-24 + time.Millisecond)
|
|
stop = time.Date(v.Year(), v.Month(), v.Day(), v.Hour(), v.Minute(), 0, 0, time.UTC)
|
|
}
|
|
|
|
where += fmt.Sprintf(` AND (c."timestamp" BETWEEN %d AND %d)`, start.UnixMilli(), stop.UnixMilli())
|
|
}
|
|
|
|
switch v := searchOptions.Limit.(type) {
|
|
case uint:
|
|
limit = uint(math.Min(float64(500), float64(v)))
|
|
default:
|
|
limit = 200
|
|
}
|
|
|
|
switch v := searchOptions.Offset.(type) {
|
|
case uint:
|
|
offset = v
|
|
}
|
|
|
|
query = fmt.Sprintf(`SELECT COUNT(*) FROM "calls" AS c LEFT JOIN "systems" AS s ON s."systemId" = c."systemId" LEFT JOIN "talkgroups" AS t ON t."talkgroupId" = c."talkgroupId" LEFT JOIN "delayed" AS d ON d."callId" = c."callId" WHERE %s`, where)
|
|
if err = db.Sql.QueryRow(query).Scan(&searchResults.Count); err != nil && err != sql.ErrNoRows {
|
|
return nil, formatError(err, query)
|
|
}
|
|
|
|
query = fmt.Sprintf(`SELECT c."callId", c."timestamp", s."systemRef", t."talkgroupRef" FROM "calls" AS c LEFT JOIN "systems" AS s ON s."systemId" = c."systemId" LEFT JOIN "talkgroups" AS t ON t."talkgroupId" = c."talkgroupId" LEFT JOIN "delayed" AS d ON d."callId" = c."callId" WHERE %s ORDER BY c."timestamp" %s LIMIT %d OFFSET %d`, where, order, limit, offset)
|
|
if rows, err = db.Sql.Query(query); err != nil && err != sql.ErrNoRows {
|
|
return nil, formatError(err, query)
|
|
}
|
|
|
|
for rows.Next() {
|
|
searchResult := CallsSearchResult{}
|
|
if err = rows.Scan(&searchResult.Id, ×tamp, &searchResult.System, &searchResult.Talkgroup); err != nil {
|
|
break
|
|
}
|
|
|
|
searchResult.Timestamp = time.UnixMilli(timestamp)
|
|
|
|
searchResults.Results = append(searchResults.Results, searchResult)
|
|
}
|
|
|
|
rows.Close()
|
|
|
|
if err != nil {
|
|
return nil, formatError(err, "")
|
|
}
|
|
|
|
return searchResults, err
|
|
}
|
|
|
|
func (calls *Calls) WriteCall(call *Call, db *Database) (uint64, error) {
|
|
var (
|
|
err error
|
|
query string
|
|
res sql.Result
|
|
tx *sql.Tx
|
|
)
|
|
|
|
calls.mutex.Lock()
|
|
defer calls.mutex.Unlock()
|
|
|
|
formatError := errorFormatter("calls", "writecall")
|
|
|
|
if tx, err = db.Sql.Begin(); err != nil {
|
|
return 0, formatError(err, "")
|
|
}
|
|
|
|
if db.Config.DbType == DbTypePostgresql {
|
|
query = fmt.Sprintf(`INSERT INTO "calls" ("audio", "audioFilename", "audioMime", "siteRef", "systemId", "talkgroupId", "timestamp") VALUES ($1, '%s', '%s', %d, %d, %d, %d) RETURNING "callId"`, call.AudioFilename, call.AudioMime, call.SiteRef, call.System.Id, call.Talkgroup.Id, call.Timestamp.UnixMilli())
|
|
|
|
err = tx.QueryRow(query, call.Audio).Scan(&call.Id)
|
|
|
|
} else {
|
|
query = fmt.Sprintf(`INSERT INTO "calls" ("audio", "audioFilename", "audioMime", "siteRef", "systemId", "talkgroupId", "timestamp") VALUES (?, '%s', '%s', %d, %d, %d, %d)`, call.AudioFilename, call.AudioMime, call.SiteRef, call.System.Id, call.Talkgroup.Id, call.Timestamp.UnixMilli())
|
|
|
|
if res, err = tx.Exec(query, call.Audio); err == nil {
|
|
if id, err := res.LastInsertId(); err == nil {
|
|
call.Id = uint64(id)
|
|
}
|
|
}
|
|
}
|
|
|
|
if err != nil {
|
|
tx.Rollback()
|
|
return 0, formatError(err, query)
|
|
}
|
|
|
|
for _, freq := range call.Frequencies {
|
|
query = fmt.Sprintf(`INSERT INTO "callFrequencies" ("callId", "dbm", "errors", "frequency", "offset", "spikes") VALUES (%d, %d, %d, %d, %f, %d)`, call.Id, freq.Dbm, freq.Errors, freq.Frequency, freq.Offset, freq.Spikes)
|
|
if _, err = tx.Exec(query); err != nil {
|
|
tx.Rollback()
|
|
return 0, formatError(err, query)
|
|
}
|
|
}
|
|
|
|
for _, ref := range call.Patches {
|
|
var talkgroupId sql.NullInt64
|
|
query = fmt.Sprintf(`SELECT "talkgroupId" FROM "talkgroups" WHERE "systemId" = %d and "talkgroupRef" = %d`, call.System.Id, ref)
|
|
if err = tx.QueryRow(query).Scan(&talkgroupId); err != nil && err != sql.ErrNoRows {
|
|
tx.Rollback()
|
|
return 0, formatError(err, query)
|
|
}
|
|
if !talkgroupId.Valid {
|
|
continue
|
|
}
|
|
query = fmt.Sprintf(`INSERT INTO "callPatches" ("callId", "talkgroupId") VALUES (%d, %d)`, call.Id, talkgroupId.Int64)
|
|
if _, err = tx.Exec(query); err != nil {
|
|
tx.Rollback()
|
|
return 0, formatError(err, query)
|
|
}
|
|
}
|
|
|
|
for _, unit := range call.Units {
|
|
query = fmt.Sprintf(`INSERT INTO "callUnits" ("callId", "offset", "unitRef") VALUES (%d, %f, %d)`, call.Id, unit.Offset, unit.UnitRef)
|
|
if _, err = tx.Exec(query); err != nil {
|
|
tx.Rollback()
|
|
return 0, formatError(err, query)
|
|
}
|
|
}
|
|
|
|
if err = tx.Commit(); err != nil {
|
|
tx.Rollback()
|
|
return 0, formatError(err, "")
|
|
}
|
|
|
|
return uint64(call.Id), nil
|
|
}
|
|
|
|
type CallsSearchOptions struct {
|
|
Date any `json:"date,omitempty"`
|
|
Group any `json:"group,omitempty"`
|
|
Limit any `json:"limit,omitempty"`
|
|
Offset any `json:"offset,omitempty"`
|
|
Sort any `json:"sort,omitempty"`
|
|
System any `json:"system,omitempty"`
|
|
Tag any `json:"tag,omitempty"`
|
|
Talkgroup any `json:"talkgroup,omitempty"`
|
|
}
|
|
|
|
func NewCallSearchOptions() *CallsSearchOptions {
|
|
return &CallsSearchOptions{}
|
|
}
|
|
|
|
func (searchOptions *CallsSearchOptions) fromMap(m map[string]any) *CallsSearchOptions {
|
|
switch v := m["date"].(type) {
|
|
case string:
|
|
if t, err := time.Parse(time.RFC3339, v); err == nil {
|
|
searchOptions.Date = t
|
|
}
|
|
}
|
|
|
|
switch v := m["group"].(type) {
|
|
case string:
|
|
searchOptions.Group = v
|
|
}
|
|
|
|
switch v := m["limit"].(type) {
|
|
case float64:
|
|
searchOptions.Limit = uint(v)
|
|
}
|
|
|
|
switch v := m["offset"].(type) {
|
|
case float64:
|
|
searchOptions.Offset = uint(v)
|
|
}
|
|
|
|
switch v := m["sort"].(type) {
|
|
case float64:
|
|
searchOptions.Sort = int(v)
|
|
}
|
|
|
|
switch v := m["system"].(type) {
|
|
case float64:
|
|
searchOptions.System = uint(v)
|
|
}
|
|
|
|
switch v := m["tag"].(type) {
|
|
case string:
|
|
searchOptions.Tag = v
|
|
}
|
|
|
|
switch v := m["talkgroup"].(type) {
|
|
case float64:
|
|
searchOptions.Talkgroup = uint(v)
|
|
}
|
|
|
|
return searchOptions
|
|
}
|
|
|
|
type CallsSearchResult struct {
|
|
Id uint64 `json:"id"`
|
|
System uint `json:"system"`
|
|
Talkgroup uint `json:"talkgroup"`
|
|
Timestamp time.Time `json:"dateTime"`
|
|
}
|
|
|
|
type CallsSearchResults struct {
|
|
Count uint `json:"count"`
|
|
DateStart time.Time `json:"dateStart"`
|
|
DateStop time.Time `json:"dateStop"`
|
|
Options *CallsSearchOptions `json:"options"`
|
|
Results []CallsSearchResult `json:"results"`
|
|
}
|