stillbox/pkg/gordio/ingestors/http.go

181 lines
4.7 KiB
Go
Raw Normal View History

2024-07-28 23:22:12 -04:00
package ingestors
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-07-29 00:29:16 -04:00
"dynatron.me/x/stillbox/pkg/gordio/auth"
2024-07-24 22:42:30 -04:00
"dynatron.me/x/stillbox/pkg/gordio/database"
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-07-29 00:29:16 -04:00
// HTTPIngestor is an ingestor that accepts calls over HTTP.
2024-07-28 23:22:12 -04:00
type HTTPIngestor struct {
2024-07-29 00:58:32 -04:00
auth auth.Authenticator
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-07-29 00:58:32 -04:00
func NewHTTPIngestor(auth auth.Authenticator) *HTTPIngestor {
2024-07-29 00:29:16 -04:00
return &HTTPIngestor{
auth: auth,
}
2024-07-28 23:22:12 -04:00
}
2024-07-29 00:29:16 -04:00
// InstallRoutes installs the HTTP ingestor's routes to the provided chi Router.
2024-07-28 23:22:12 -04:00
func (h *HTTPIngestor) InstallRoutes(r chi.Router) {
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-07-29 00:29:16 -04:00
func (car *callUploadRequest) toAddCallParams(submitter int) database.AddCallParams {
2024-07-25 09:37:27 -04:00
return database.AddCallParams{
2024-07-27 19:25:16 -04:00
Submitter: common.PtrTo(int32(submitter)),
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,
2024-07-25 09:37:27 -04:00
}
}
2024-07-28 23:22:12 -04:00
func (h *HTTPIngestor) 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-07-29 00:29:16 -04:00
apik, err := h.auth.CheckAPIKey(ctx, r.Form.Get("key"))
if err != nil {
auth.ErrorResponse(w, err)
2024-07-24 22:42:30 -04:00
return
}
2024-07-29 00:29:16 -04:00
db := database.FromCtx(ctx)
2024-07-24 22:42:30 -04:00
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-07-24 14:07:24 -04:00
call := new(callUploadRequest)
2024-07-24 22:42:30 -04:00
err = call.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-07-29 00:29:16 -04:00
dbCall, err := db.AddCall(ctx, call.toAddCallParams(apik.Owner))
2024-07-25 09:37:27 -04:00
if err != nil {
http.Error(w, "internal error", http.StatusInternalServerError)
log.Error().Err(err).Msg("add call")
return
}
2024-07-27 19:25:16 -04:00
log.Info().Str("id", dbCall.String()).Int("system", call.System).Int("tgid", call.Talkgroup).Msg("ingested")
2024-07-28 01:08:08 -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:
val, err := strconv.Atoi(r.Form.Get(formField))
2024-07-24 18:18:04 -04:00
if err != nil {
2024-07-24 22:49:42 -04:00
return fmt.Errorf("atoi('%s'): %w", formField, 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-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
}