Init with working version
Basic demo for Tally X AutoIxpert integration to showcase setup in Go due to impossibility of open-sourcing n8n setup.
This commit is contained in:
249
api.go
Normal file
249
api.go
Normal file
@@ -0,0 +1,249 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"image"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"git.kornbrand.biz/plankalkul/autoixpert-sample/pkg/models"
|
||||
)
|
||||
|
||||
type apiError struct {
|
||||
StatusCode int `json:"status_code"`
|
||||
Endpoint string `json:"endpoint"`
|
||||
ErrorCode string `json:"error_code"`
|
||||
ErrorMessage string `json:"error_message"`
|
||||
StackTrace string `json:"stack_trace"`
|
||||
}
|
||||
|
||||
// could be more elaborate depending on the users needs, for simplicity just the API error messageu
|
||||
// depending on usecase, might be useful to include all fields of this in the slog as parsebable output
|
||||
func (e *apiError) Error() string {
|
||||
if e.ErrorMessage == "" {
|
||||
return fmt.Sprintf("api request: %s failed with status %d", e.Endpoint, e.StatusCode)
|
||||
}
|
||||
return e.ErrorMessage
|
||||
}
|
||||
|
||||
func decodeAPIError(resp *http.Response) error {
|
||||
body, readErr := io.ReadAll(resp.Body)
|
||||
if readErr != nil {
|
||||
return fmt.Errorf("api request failed with status %d (failed to read error response: %w)", resp.StatusCode, readErr)
|
||||
}
|
||||
|
||||
var apiErr apiError
|
||||
if err := json.Unmarshal(body, &apiErr); err != nil {
|
||||
return fmt.Errorf("api request failed with status %d (failed to decode error response: %w)", resp.StatusCode, err)
|
||||
}
|
||||
|
||||
return &apiErr
|
||||
}
|
||||
|
||||
func createReport(report models.CrashReport) (string, error) {
|
||||
|
||||
var body = report.ToClaimRequest()
|
||||
data, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", cfg.APIBase+"/reports", bytes.NewBuffer(data))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+cfg.APIKey)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := DoWithRateLimitReset(context.Background(), req)
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode >= 400 {
|
||||
return "", decodeAPIError(resp)
|
||||
}
|
||||
|
||||
var claimResp models.ClaimResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&claimResp); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return claimResp.Document.ID, nil
|
||||
}
|
||||
|
||||
func uploadImages(imageURLs []string, id string, eventID string) error {
|
||||
images, err := collectImages(imageURLs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
metaResp, err := requestPhotoMetadata(id, eventID, images)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
uploadResp, err := requestUploadURLs(id, eventID, metaResp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return uploadPhotoBinaries(images, metaResp, uploadResp)
|
||||
}
|
||||
|
||||
func collectImages(imageURLs []string) ([]imageWithMeta, error) {
|
||||
images := make([]imageWithMeta, 0, len(imageURLs))
|
||||
|
||||
for idx, url := range imageURLs {
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data, err := io.ReadAll(resp.Body)
|
||||
_ = resp.Body.Close()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mime := http.DetectContentType(data)
|
||||
size := len(data)
|
||||
cfg, _, err := image.DecodeConfig(bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
images = append(images, imageWithMeta{
|
||||
Data: data,
|
||||
Meta: models.ImageMetaRequest{
|
||||
Title: strconv.Itoa(idx),
|
||||
Description: "",
|
||||
OriginalName: strconv.Itoa(idx),
|
||||
Mimetype: mime,
|
||||
Size: int64(size),
|
||||
Width: cfg.Width,
|
||||
Height: cfg.Height,
|
||||
IncludedInReport: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return images, nil
|
||||
}
|
||||
|
||||
func requestPhotoMetadata(reportID string, eventID string, images []imageWithMeta) (models.ImageMetaResponse, error) {
|
||||
metas := make([]models.ImageMetaRequest, 0, len(images))
|
||||
for _, img := range images {
|
||||
metas = append(metas, img.Meta)
|
||||
}
|
||||
|
||||
body, err := json.Marshal(models.ImageMetaRequestWrapper{Photos: metas})
|
||||
if err != nil {
|
||||
return models.ImageMetaResponse{}, err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", cfg.APIBase+"/reports/"+reportID+"/photos/batch", bytes.NewBuffer(body))
|
||||
if err != nil {
|
||||
return models.ImageMetaResponse{}, err
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+cfg.APIKey)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := DoWithRateLimitReset(context.Background(), req)
|
||||
if err != nil {
|
||||
return models.ImageMetaResponse{}, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode >= 400 {
|
||||
return models.ImageMetaResponse{}, decodeAPIError(resp)
|
||||
}
|
||||
|
||||
var metaResp models.ImageMetaResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&metaResp); err != nil {
|
||||
return models.ImageMetaResponse{}, err
|
||||
}
|
||||
|
||||
return metaResp, nil
|
||||
}
|
||||
|
||||
func requestUploadURLs(reportID string, eventID string, metaResp models.ImageMetaResponse) (models.UploadURLResponse, error) {
|
||||
ids := make([]string, 0, len(metaResp.Photos))
|
||||
for _, photo := range metaResp.Photos {
|
||||
ids = append(ids, photo.ID)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", cfg.APIBase+"/reports/"+reportID+"/photos/batch/upload?photo_ids="+strings.Join(ids, ","), nil)
|
||||
if err != nil {
|
||||
return models.UploadURLResponse{}, err
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+cfg.APIKey)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := DoWithRateLimitReset(context.Background(), req)
|
||||
if err != nil {
|
||||
return models.UploadURLResponse{}, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode >= 400 {
|
||||
return models.UploadURLResponse{}, decodeAPIError(resp)
|
||||
}
|
||||
|
||||
var uploadResp models.UploadURLResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&uploadResp); err != nil {
|
||||
return models.UploadURLResponse{}, err
|
||||
}
|
||||
|
||||
return uploadResp, nil
|
||||
}
|
||||
|
||||
func uploadPhotoBinaries(images []imageWithMeta, metaResp models.ImageMetaResponse, uploadResp models.UploadURLResponse) error {
|
||||
imageByTitle := make(map[string]imageWithMeta, len(images))
|
||||
for _, img := range images {
|
||||
imageByTitle[img.Meta.Title] = img
|
||||
}
|
||||
|
||||
titleByPhotoID := make(map[string]string, len(metaResp.Photos))
|
||||
for _, metaImg := range metaResp.Photos {
|
||||
titleByPhotoID[metaImg.ID] = metaImg.Title
|
||||
}
|
||||
|
||||
for _, upload := range uploadResp.UploadURLs {
|
||||
title, ok := titleByPhotoID[upload.PhotoID]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
img, ok := imageByTitle[title]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
putReq, err := http.NewRequest("PUT", upload.UploadURL, bytes.NewReader(img.Data))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
putReq.Header.Set("Content-Type", img.Meta.Mimetype)
|
||||
|
||||
resp, err := Do(context.Background(), putReq)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode >= 400 {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user