2024-08-01 01:01:08 -04:00
|
|
|
package sources
|
2024-07-24 08:38:18 -04:00
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
2024-07-24 14:07:24 -04:00
|
|
|
"io"
|
2024-07-24 08:38:18 -04:00
|
|
|
"net/http"
|
2024-07-24 14:07:24 -04:00
|
|
|
"reflect"
|
2024-07-24 18:18:04 -04:00
|
|
|
"strconv"
|
|
|
|
"strings"
|
2024-07-24 08:38:18 -04:00
|
|
|
"time"
|
|
|
|
|
2024-07-27 19:25:16 -04:00
|
|
|
"dynatron.me/x/stillbox/internal/common"
|
2024-08-05 18:11:31 -04:00
|
|
|
"dynatron.me/x/stillbox/pkg/calls"
|
2024-08-06 11:19:30 -04:00
|
|
|
"dynatron.me/x/stillbox/pkg/gordio/auth"
|
2024-07-28 23:22:12 -04:00
|
|
|
"github.com/go-chi/chi/v5"
|
2024-07-25 09:37:27 -04:00
|
|
|
"github.com/rs/zerolog/log"
|
2024-07-24 22:42:30 -04:00
|
|
|
)
|
2024-07-24 14:07:24 -04:00
|
|
|
|
2024-08-01 01:01:08 -04:00
|
|
|
// RdioHTTP is an source that accepts calls using the rdio-scanner HTTP interface.
|
|
|
|
type RdioHTTP struct {
|
2024-07-29 00:58:32 -04:00
|
|
|
auth auth.Authenticator
|
2024-08-01 01:01:08 -04:00
|
|
|
ing Ingestor
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r *RdioHTTP) SourceType() string {
|
|
|
|
return "rdio-http"
|
2024-07-28 23:22:12 -04:00
|
|
|
}
|
|
|
|
|
2024-07-29 00:29:16 -04:00
|
|
|
// NewHTTPIngestor creates a new HTTPIngestor. It requires an Authenticator.
|
2024-08-01 01:01:08 -04:00
|
|
|
func NewRdioHTTP(auth auth.Authenticator, ing Ingestor) *RdioHTTP {
|
|
|
|
return &RdioHTTP{
|
2024-07-29 00:29:16 -04:00
|
|
|
auth: auth,
|
2024-08-01 01:01:08 -04:00
|
|
|
ing: ing,
|
2024-07-29 00:29:16 -04:00
|
|
|
}
|
2024-07-28 23:22:12 -04:00
|
|
|
}
|
|
|
|
|
2024-08-01 01:01:08 -04:00
|
|
|
// InstallPublicRoutes installs the HTTP source's routes to the provided chi Router.
|
|
|
|
func (h *RdioHTTP) InstallPublicRoutes(r chi.Router) {
|
2024-07-28 23:22:12 -04:00
|
|
|
r.Post("/api/call-upload", h.routeCallUpload)
|
|
|
|
}
|
|
|
|
|
2024-07-24 08:38:18 -04:00
|
|
|
type callUploadRequest struct {
|
2024-07-28 01:08:08 -04:00
|
|
|
Audio []byte `form:"audio"`
|
|
|
|
AudioName string
|
2024-07-24 14:07:24 -04:00
|
|
|
AudioType string `form:"audioType"`
|
|
|
|
DateTime time.Time `form:"dateTime"`
|
2024-07-24 08:38:18 -04:00
|
|
|
Frequencies []int `form:"frequencies"`
|
|
|
|
Frequency int `form:"frequency"`
|
|
|
|
Key string `form:"key"`
|
2024-07-24 22:49:42 -04:00
|
|
|
Patches []int `form:"patches"`
|
2024-07-24 22:42:30 -04:00
|
|
|
Source int `form:"source"`
|
2024-07-24 22:49:42 -04:00
|
|
|
Sources []int `form:"sources"`
|
2024-07-24 22:42:30 -04:00
|
|
|
System int `form:"system"`
|
2024-07-24 08:38:18 -04:00
|
|
|
SystemLabel string `form:"systemLabel"`
|
|
|
|
Talkgroup int `form:"talkgroup"`
|
|
|
|
TalkgroupGroup string `form:"talkgroupGroup"`
|
|
|
|
TalkgroupLabel string `form:"talkgroupLabel"`
|
|
|
|
TalkgroupTag string `form:"talkgroupTag"`
|
2024-08-18 08:44:44 -04:00
|
|
|
DontStore bool `form:"dontStore"`
|
2024-07-24 08:38:18 -04:00
|
|
|
}
|
|
|
|
|
2024-08-01 01:01:08 -04:00
|
|
|
func (car *callUploadRequest) mimeType() string {
|
|
|
|
// this is super naïve
|
|
|
|
fn := car.AudioName
|
|
|
|
switch {
|
|
|
|
case car.AudioType != "":
|
|
|
|
return car.AudioType
|
|
|
|
case strings.HasSuffix(fn, ".mp3"):
|
|
|
|
return "audio/mpeg"
|
|
|
|
case strings.HasSuffix(fn, ".wav"):
|
|
|
|
return "audio/wav"
|
|
|
|
}
|
|
|
|
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
2024-08-14 09:32:53 -04:00
|
|
|
func (car *callUploadRequest) toCall(submitter auth.UserID) (*calls.Call, error) {
|
|
|
|
return calls.Make(&calls.Call{
|
2024-08-01 01:01:08 -04:00
|
|
|
Submitter: &submitter,
|
|
|
|
System: car.System,
|
|
|
|
Talkgroup: car.Talkgroup,
|
|
|
|
DateTime: car.DateTime,
|
|
|
|
AudioName: car.AudioName,
|
|
|
|
Audio: car.Audio,
|
|
|
|
AudioType: car.mimeType(),
|
|
|
|
Frequency: car.Frequency,
|
|
|
|
Frequencies: car.Frequencies,
|
|
|
|
Patches: car.Patches,
|
|
|
|
TalkgroupLabel: common.PtrOrNull(car.TalkgroupLabel),
|
|
|
|
TalkgroupTag: common.PtrOrNull(car.TalkgroupTag),
|
|
|
|
TalkgroupGroup: common.PtrOrNull(car.TalkgroupGroup),
|
|
|
|
Source: car.Source,
|
2024-08-18 08:44:44 -04:00
|
|
|
}, !car.DontStore)
|
2024-07-25 09:37:27 -04:00
|
|
|
}
|
|
|
|
|
2024-08-01 01:01:08 -04:00
|
|
|
func (h *RdioHTTP) routeCallUpload(w http.ResponseWriter, r *http.Request) {
|
2024-07-24 22:42:30 -04:00
|
|
|
err := r.ParseMultipartForm(1024 * 1024 * 2) // 2MB
|
|
|
|
if err != nil {
|
|
|
|
http.Error(w, "cannot parse form "+err.Error(), http.StatusBadRequest)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2024-07-29 00:29:16 -04:00
|
|
|
ctx := r.Context()
|
2024-07-24 22:42:30 -04:00
|
|
|
|
2024-08-01 01:01:08 -04:00
|
|
|
submitter, err := h.auth.CheckAPIKey(ctx, r.Form.Get("key"))
|
2024-07-29 00:29:16 -04:00
|
|
|
if err != nil {
|
|
|
|
auth.ErrorResponse(w, err)
|
2024-07-24 22:42:30 -04:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2024-07-28 01:08:08 -04:00
|
|
|
if strings.Trim(r.Form.Get("test"), "\r\n") == "1" {
|
|
|
|
// fudge the official response
|
|
|
|
http.Error(w, "incomplete call data: no talkgroup", http.StatusExpectationFailed)
|
2024-07-27 21:27:48 -04:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2024-08-01 01:01:08 -04:00
|
|
|
cur := new(callUploadRequest)
|
|
|
|
err = cur.fill(r)
|
2024-07-24 08:38:18 -04:00
|
|
|
if err != nil {
|
2024-07-28 01:08:08 -04:00
|
|
|
http.Error(w, "cannot bind upload "+err.Error(), http.StatusExpectationFailed)
|
2024-07-24 08:38:18 -04:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2024-08-14 09:32:53 -04:00
|
|
|
call, err := cur.toCall(*submitter)
|
|
|
|
if err != nil {
|
|
|
|
log.Error().Err(err).Msg("toCall failed")
|
|
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
h.ing.Ingest(ctx, call)
|
2024-07-27 19:25:16 -04:00
|
|
|
|
2024-08-01 01:01:08 -04:00
|
|
|
log.Info().Int("system", cur.System).Int("tgid", cur.Talkgroup).Msg("ingested")
|
2024-07-28 01:08:08 -04:00
|
|
|
|
2024-08-11 14:48:17 -04:00
|
|
|
_, _ = w.Write([]byte("Call imported successfully."))
|
2024-07-24 08:38:18 -04:00
|
|
|
}
|
2024-07-24 14:07:24 -04:00
|
|
|
|
2024-07-24 18:18:04 -04:00
|
|
|
func (car *callUploadRequest) fill(r *http.Request) error {
|
|
|
|
rv := reflect.ValueOf(car).Elem()
|
2024-07-24 14:07:24 -04:00
|
|
|
rt := rv.Type()
|
|
|
|
|
|
|
|
for i := 0; i < rv.NumField(); i++ {
|
|
|
|
f := rv.Field(i)
|
2024-07-24 22:49:42 -04:00
|
|
|
fi := f.Interface()
|
2024-07-28 01:08:08 -04:00
|
|
|
formField, has := rt.Field(i).Tag.Lookup("form")
|
|
|
|
if !has {
|
|
|
|
continue
|
|
|
|
}
|
2024-07-24 22:49:42 -04:00
|
|
|
switch v := fi.(type) {
|
|
|
|
case []byte:
|
2024-07-28 01:08:08 -04:00
|
|
|
file, hdr, err := r.FormFile(formField)
|
2024-07-24 14:07:24 -04:00
|
|
|
if err != nil {
|
2024-07-24 18:18:04 -04:00
|
|
|
return fmt.Errorf("get form file: %w", err)
|
2024-07-24 14:07:24 -04:00
|
|
|
}
|
|
|
|
|
2024-07-28 01:08:08 -04:00
|
|
|
car.AudioName = hdr.Filename
|
2024-07-24 14:07:24 -04:00
|
|
|
audioBytes, err := io.ReadAll(file)
|
|
|
|
if err != nil {
|
2024-07-24 18:18:04 -04:00
|
|
|
return fmt.Errorf("file read: %w", err)
|
2024-07-24 14:07:24 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
f.SetBytes(audioBytes)
|
2024-07-24 22:49:42 -04:00
|
|
|
case time.Time:
|
2024-07-27 19:25:16 -04:00
|
|
|
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)
|
2024-07-24 14:07:24 -04:00
|
|
|
if err != nil {
|
2024-07-24 18:18:04 -04:00
|
|
|
return fmt.Errorf("parse time: %w", err)
|
2024-07-24 14:07:24 -04:00
|
|
|
}
|
|
|
|
f.Set(reflect.ValueOf(t))
|
2024-07-24 22:49:42 -04:00
|
|
|
case []int:
|
|
|
|
val := strings.Trim(r.Form.Get(formField), "[]")
|
2024-07-24 18:18:04 -04:00
|
|
|
if val == "" {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
vals := strings.Split(val, ",")
|
|
|
|
ar := make([]int, 0, len(vals))
|
|
|
|
for _, v := range vals {
|
|
|
|
i, err := strconv.Atoi(v)
|
|
|
|
if err == nil {
|
|
|
|
ar = append(ar, i)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
f.Set(reflect.ValueOf(ar))
|
2024-07-24 22:49:42 -04:00
|
|
|
case int:
|
2024-08-18 08:44:44 -04:00
|
|
|
ff := r.Form.Get(formField)
|
|
|
|
val, err := strconv.Atoi(ff)
|
2024-07-24 18:18:04 -04:00
|
|
|
if err != nil {
|
2024-08-18 08:44:44 -04:00
|
|
|
return fmt.Errorf("atoi('%s'): %w", ff, err)
|
2024-07-24 18:18:04 -04:00
|
|
|
}
|
|
|
|
f.SetInt(int64(val))
|
2024-07-24 22:49:42 -04:00
|
|
|
case string:
|
|
|
|
f.SetString(r.Form.Get(formField))
|
2024-08-18 08:44:44 -04:00
|
|
|
case bool:
|
|
|
|
ff := r.Form.Get(formField)
|
|
|
|
if ff == "" {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
val, err := strconv.ParseBool(ff)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("parsebool('%s'): %w", ff, err)
|
|
|
|
}
|
|
|
|
f.SetBool(val)
|
2024-07-24 14:07:24 -04:00
|
|
|
default:
|
2024-07-24 22:49:42 -04:00
|
|
|
panic(fmt.Errorf("unsupported type %T", v))
|
2024-07-24 14:07:24 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|