nidus-sync/api/api.go
Eli Ribble 2538638c9d
Create generic backend process, fix background interdependencies
This refactor was born out of the inter-dependency cycles developing
between the "background" module and just about every other module which
was caused by the background module becoming a dependency of every
module that needed to background work and the fact that the background
module was also supposedly responsible for the logic for processing
those tasks.

Instead the "background" module is now very, very shallow and relies
entirely on the Postgres NOTIFY logic for triggering jobs. There's a new
table, `job` which holds just a type and single row ID.

All told, this means that jobs can be added to the queue as part of the
API-level or platform-level transaction, ensuring atomicity, and
processing coordination is handled by the platform module, which can
depend on anything.
2026-03-16 19:52:29 +00:00

302 lines
7.7 KiB
Go

package api
import (
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strconv"
"time"
"github.com/Gleipnir-Technology/nidus-sync/db"
"github.com/Gleipnir-Technology/nidus-sync/db/models"
"github.com/Gleipnir-Technology/nidus-sync/platform"
"github.com/Gleipnir-Technology/nidus-sync/platform/background"
"github.com/Gleipnir-Technology/nidus-sync/platform/file"
"github.com/aarondl/opt/omit"
"github.com/aarondl/opt/omitnull"
"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
"github.com/google/uuid"
"github.com/rs/zerolog/log"
)
func apiAudioPost(w http.ResponseWriter, r *http.Request, u platform.User) {
id := chi.URLParam(r, "uuid")
noteUUID, err := uuid.Parse(id)
if err != nil {
http.Error(w, "Failed to decode the uuid", http.StatusBadRequest)
return
}
var payload NoteAudioPayload
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "Failed to read the payload", http.StatusBadRequest)
return
}
if err := json.Unmarshal(body, &payload); err != nil {
//debugSaveRequest(body, err, "Audio note POST JSON decode error")
http.Error(w, "Failed to decode the payload", http.StatusBadRequest)
return
}
ctx := r.Context()
setter := models.NoteAudioSetter{
Created: omit.From(payload.Created),
CreatorID: omit.From(int32(u.ID)),
Deleted: omitnull.FromPtr(payload.Deleted),
DeletorID: omitnull.FromPtr(payload.DeletorID),
Duration: omit.From(payload.Duration),
OrganizationID: omit.From(u.Organization.ID()),
Transcription: omitnull.FromPtr(payload.Transcription),
TranscriptionUserEdited: omit.From(payload.TranscriptionUserEdited),
Version: omit.From(payload.Version),
UUID: omit.From(noteUUID),
}
if err := platform.NoteAudioCreate(ctx, u, setter); err != nil {
render.Render(w, r, errRender(err))
return
}
w.WriteHeader(http.StatusAccepted)
}
func apiAudioContentPost(w http.ResponseWriter, r *http.Request, user platform.User) {
u_str := chi.URLParam(r, "uuid")
u, err := uuid.Parse(u_str)
if err != nil {
http.Error(w, "Failed to parse image UUID", http.StatusBadRequest)
return
}
err = file.FileContentWrite(r.Body, file.CollectionAudioRaw, u)
if err != nil {
log.Printf("Failed to write content file: %v", err)
http.Error(w, "failed to write content file", http.StatusInternalServerError)
}
ctx := r.Context()
a, err := models.NoteAudios.Query(
models.SelectWhere.NoteAudios.UUID.EQ(u),
models.SelectWhere.NoteAudios.OrganizationID.EQ(user.Organization.ID()),
).One(ctx, db.PGInstance.BobDB)
background.NewAudioTranscode(ctx, db.PGInstance.BobDB, a.ID)
w.WriteHeader(http.StatusOK)
}
func handleClientIos(w http.ResponseWriter, r *http.Request, u platform.User) {
var sinceStr string
err := r.ParseForm()
if err != nil {
render.Render(w, r, errRender(fmt.Errorf("Failed to parse GET form: %w", err)))
return
} else {
sinceStr = r.FormValue("since")
}
var since *time.Time
if sinceStr == "" {
since = nil
} else {
since, err = parseTime(sinceStr)
if err != nil {
render.Render(w, r, errRender(fmt.Errorf("Failed to parse 'since' value: %w", err)))
return
}
}
csync, err := platform.ContentClientIos(r.Context(), u, since)
if err != nil {
render.Render(w, r, errRender(err))
return
}
var since_used time.Time
if since == nil {
since_used = time.Unix(0, 0)
} else {
since_used = *since
}
response := ResponseClientIos{
Fieldseeker: toResponseFieldseeker(csync.Fieldseeker),
Since: since_used,
}
if err := render.Render(w, r, response); err != nil {
render.Render(w, r, errRender(err))
return
}
}
func apiMosquitoSource(w http.ResponseWriter, r *http.Request, u platform.User) {
bounds, err := parseBounds(r)
if err != nil {
render.Render(w, r, errRender(err))
return
}
query := db.NewGeoQuery()
query.Bounds = *bounds
query.Limit = 100
sources, err := platform.MosquitoSourceQuery()
if err != nil {
render.Render(w, r, errRender(err))
return
}
data := []render.Renderer{}
for _, s := range sources {
data = append(data, NewResponseMosquitoSource(s))
}
if err := render.RenderList(w, r, data); err != nil {
render.Render(w, r, errRender(err))
}
}
func apiTrapData(w http.ResponseWriter, r *http.Request, u platform.User) {
bounds, err := parseBounds(r)
if err != nil {
render.Render(w, r, errRender(err))
return
}
query := db.NewGeoQuery()
query.Bounds = *bounds
query.Limit = 100
trap_data, err := platform.TrapDataQuery()
if err != nil {
render.Render(w, r, errRender(err))
return
}
data := []render.Renderer{}
for _, td := range trap_data {
data = append(data, NewResponseTrapDatum(td))
}
if err := render.RenderList(w, r, data); err != nil {
render.Render(w, r, errRender(err))
}
}
func apiServiceRequest(w http.ResponseWriter, r *http.Request, u platform.User) {
bounds, err := parseBounds(r)
if err != nil {
render.Render(w, r, errRender(err))
return
}
query := db.NewGeoQuery()
query.Bounds = *bounds
query.Limit = 100
requests, err := platform.ServiceRequestQuery()
if err != nil {
render.Render(w, r, errRender(err))
return
}
data := []render.Renderer{}
for _, sr := range requests {
data = append(data, NewResponseServiceRequest(sr))
}
if err := render.RenderList(w, r, data); err != nil {
render.Render(w, r, errRender(err))
}
}
func parseBounds(r *http.Request) (*db.GeoBounds, error) {
err := r.ParseForm()
if err != nil {
return nil, err
}
east := r.FormValue("east")
north := r.FormValue("north")
south := r.FormValue("south")
west := r.FormValue("west")
bounds := db.GeoBounds{}
var temp float64
temp, err = strconv.ParseFloat(east, 64)
if err != nil {
return nil, err
}
bounds.East = temp
temp, err = strconv.ParseFloat(north, 64)
if err != nil {
return nil, err
}
bounds.North = temp
temp, err = strconv.ParseFloat(south, 64)
if err != nil {
return nil, err
}
bounds.South = temp
temp, err = strconv.ParseFloat(west, 64)
if err != nil {
return nil, err
}
bounds.West = temp
return &bounds, nil
}
func errRender(err error) render.Renderer {
log.Error().Err(err).Msg("Rendering error")
return &ResponseErr{
Error: err,
HTTPStatusCode: 500,
StatusText: "Error rendering response",
ErrorText: err.Error(),
}
}
func webhookFieldseeker(w http.ResponseWriter, r *http.Request) {
// Create or open the log file
file, err := os.OpenFile("webhook/request.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
log.Printf("Error opening log file: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
defer file.Close()
// Write timestamp
timestamp := time.Now().Format("2006-01-02 15:04:05")
fmt.Fprintf(file, "\n=== Request logged at %s ===\n", timestamp)
// Write request line
fmt.Fprintf(file, "%s %s %s\n", r.Method, r.RequestURI, r.Proto)
// Write all headers
fmt.Fprintf(file, "\nHeaders:\n")
for name, values := range r.Header {
for _, value := range values {
fmt.Fprintf(file, "%s: %s\n", name, value)
}
}
// Write body
fmt.Fprintf(file, "\nBody:\n")
body, err := io.ReadAll(r.Body)
if err != nil {
log.Printf("Error reading request body: %v", err)
fmt.Fprintf(file, "Error reading body: %v\n", err)
} else {
file.Write(body)
if len(body) == 0 {
fmt.Fprintf(file, "(empty body)")
}
}
fmt.Fprintf(file, "\n=== End of request ===\n\n")
// Extract the crc_token value for the signature portion
// Respond with 204 No Content
w.WriteHeader(http.StatusNoContent)
}
func parseTime(x string) (*time.Time, error) {
created_epoch, err := strconv.ParseInt(x, 10, 64)
if err != nil {
return &time.Time{}, fmt.Errorf("Failed to parse time '%s': %w", x, err)
}
created := time.UnixMilli(created_epoch)
return &created, nil
}