nidus-sync/api/api.go
Eli Ribble fdd783c19c
Rework userfile yet again
I'm settling on the idea that strings should never be returned from the
userfile system. Instead, indicate which collection you want and pass
objects across.
2026-02-08 04:36:12 +00:00

350 lines
9.1 KiB
Go

package api
import (
"context"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"strconv"
"time"
"github.com/Gleipnir-Technology/nidus-sync/background"
"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/userfile"
"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 *models.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 := ioutil.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
}
setter := models.NoteAudioSetter{
Created: omit.From(payload.Created),
CreatorID: omit.From(u.ID),
Deleted: omitnull.FromPtr(payload.Deleted),
DeletorID: omitnull.FromPtr(payload.DeletorID),
Duration: omit.From(payload.Duration),
Transcription: omitnull.FromPtr(payload.Transcription),
TranscriptionUserEdited: omit.From(payload.TranscriptionUserEdited),
Version: omit.From(payload.Version),
UUID: omit.From(noteUUID),
}
if err := db.NoteAudioCreate(context.Background(), u.R.Organization, u.ID, setter); err != nil {
render.Render(w, r, errRender(err))
return
}
w.WriteHeader(http.StatusAccepted)
}
func apiAudioContentPost(w http.ResponseWriter, r *http.Request, u *models.User) {
u_str := chi.URLParam(r, "uuid")
audioUUID, err := uuid.Parse(u_str)
if err != nil {
http.Error(w, "Failed to parse image UUID", http.StatusBadRequest)
return
}
err = userfile.FileContentWrite(r.Body, userfile.CollectionAudioRaw, audioUUID)
if err != nil {
log.Printf("Failed to write content file: %v", err)
http.Error(w, "failed to write content file", http.StatusInternalServerError)
}
background.AudioTranscode(audioUUID)
w.WriteHeader(http.StatusOK)
}
func handleClientIos(w http.ResponseWriter, r *http.Request, u *models.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 apiImagePost(w http.ResponseWriter, r *http.Request, u *models.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 NoteImagePayload
body, err := ioutil.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, "Image note POST JSON decode error")
http.Error(w, "Failed to decode the payload", http.StatusBadRequest)
return
}
setter := models.NoteImageSetter{
Created: omit.From(payload.Created),
CreatorID: omit.From(u.ID),
Deleted: omitnull.FromPtr(payload.Deleted),
DeletorID: omitnull.FromPtr(payload.DeletorID),
Version: omit.From(payload.Version),
UUID: omit.From(noteUUID),
}
err = db.NoteImageCreate(context.Background(), u.R.Organization, u.ID, setter)
if err != nil {
render.Render(w, r, errRender(err))
return
}
w.WriteHeader(http.StatusAccepted)
}
func apiImageContentPost(w http.ResponseWriter, r *http.Request, u *models.User) {
u_str := chi.URLParam(r, "uuid")
imageUUID, err := uuid.Parse(u_str)
if err != nil {
log.Error().Err(err).Msg("Failed to parse image UUID")
http.Error(w, "Failed to parse image UUID", http.StatusBadRequest)
}
err = userfile.ImageFileContentWrite(imageUUID, r.Body)
if err != nil {
render.Render(w, r, errRender(err))
return
}
w.WriteHeader(http.StatusOK)
log.Printf("Saved image file %s\n", imageUUID)
fmt.Fprintf(w, "PNG uploaded successfully")
}
func apiMosquitoSource(w http.ResponseWriter, r *http.Request, u *models.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 *models.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 *models.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
}