package divera import ( "bytes" "context" "encoding/json" "fmt" "io" "mime/multipart" "net/http" "net/url" "strconv" "strings" "time" "github.com/example/notify-gateway/internal/config" "github.com/example/notify-gateway/internal/outbound" ) type Client struct{ cfg func() config.DiveraConfig } type Response struct { RetryAfter time.Duration StatusCode int ContentType string Body []byte } func New(cfg func() config.DiveraConfig) *Client { return &Client{cfg: cfg} } func (c *Client) Request(ctx context.Context, method, path string, query url.Values, body any) (Response, error) { cfg := c.cfg() if cfg.DryRun && method != http.MethodGet { b, _ := json.Marshal(map[string]any{"dry_run": true, "method": method, "path": path, "body": body}) return Response{StatusCode: 200, ContentType: "application/json", Body: b}, nil } if cfg.AccessKey == "" { return Response{}, fmt.Errorf("Divera247 access_key is empty") } key, err := config.Secret(cfg.AccessKey) if err != nil { return Response{}, err } cfg.AccessKey = key base := strings.TrimRight(cfg.BaseURL, "/") u, err := url.Parse(base + path) if err != nil { return Response{}, err } q := u.Query() for k, vals := range query { for _, v := range vals { q.Add(k, v) } } q.Set("accesskey", cfg.AccessKey) // The v2 pull API requires the configured UCR. v3 uses the accesskey and // clusterId instead; sending an unrelated ucr parameter there can lead to // confusing responses, so only inject it for v2 calls. if strings.HasPrefix(path, "/api/v2/") && cfg.UCR != 0 && q.Get("ucr") == "" { q.Set("ucr", strconv.FormatInt(cfg.UCR, 10)) } u.RawQuery = q.Encode() var r io.Reader if body != nil { b, err := json.Marshal(body) if err != nil { return Response{}, err } r = bytes.NewReader(b) } req, err := http.NewRequestWithContext(ctx, method, u.String(), r) if err != nil { return Response{}, err } if body != nil { req.Header.Set("Content-Type", "application/json") } to := time.Duration(cfg.TimeoutS) * time.Second if to <= 0 { to = 15 * time.Second } resp, err := (&http.Client{Timeout: to, CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }}).Do(req) if err != nil { return Response{}, fmt.Errorf("Divera247 HTTP request failed (connection or timeout)") } defer resp.Body.Close() b, err := io.ReadAll(io.LimitReader(resp.Body, 8<<20)) if err != nil { return Response{}, err } if resp.StatusCode < 200 || resp.StatusCode >= 300 { return Response{StatusCode: resp.StatusCode, RetryAfter: outbound.ParseRetryAfter(resp.Header.Get("Retry-After"), time.Now()), ContentType: resp.Header.Get("Content-Type")}, fmt.Errorf("Divera247 API HTTP %d", resp.StatusCode) } return Response{StatusCode: resp.StatusCode, ContentType: resp.Header.Get("Content-Type"), Body: b}, nil } func (c *Client) List(ctx context.Context, kind string) (Response, error) { return c.Request(ctx, http.MethodGet, "/api/v2/"+kind, nil, nil) } func (c *Client) Create(ctx context.Context, kind string, body any) (Response, error) { return c.Request(ctx, http.MethodPost, "/api/v2/"+kind, nil, body) } func (c *Client) Get(ctx context.Context, kind string, id int64) (Response, error) { return c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/%s/%d", kind, id), nil, nil) } func (c *Client) Update(ctx context.Context, kind string, id int64, body any) (Response, error) { return c.Request(ctx, http.MethodPut, fmt.Sprintf("/api/v2/%s/%d", kind, id), nil, body) } func (c *Client) Delete(ctx context.Context, kind string, id int64) (Response, error) { return c.Request(ctx, http.MethodDelete, fmt.Sprintf("/api/v2/%s/%d", kind, id), nil, nil) } func (c *Client) Archive(ctx context.Context, kind string, id int64) (Response, error) { return c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/%s/archive/%d", kind, id), nil, nil) } func (c *Client) Read(ctx context.Context, kind string, id int64) (Response, error) { return c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/%s/read/%d", kind, id), nil, nil) } func (c *Client) Reach(ctx context.Context, kind string, id int64) (Response, error) { return c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/%s/reach/%d", kind, id), nil, nil) } func (c *Client) ResetResponses(ctx context.Context, kind string, id int64) (Response, error) { return c.Request(ctx, http.MethodDelete, fmt.Sprintf("/api/v2/%s/reset-responses/%d", kind, id), nil, nil) } func (c *Client) Confirm(ctx context.Context, kind string, id int64, body any) (Response, error) { return c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/%s/confirm/%d", kind, id), nil, body) } func (c *Client) Download(ctx context.Context, kind string, id int64) (Response, error) { return c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/%s/download/%d", kind, id), nil, nil) } func (c *Client) CloseAlarm(ctx context.Context, id int64, body any) (Response, error) { return c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/alarms/close/%d", id), nil, body) } func (c *Client) AlarmList(ctx context.Context, closed *int) (Response, error) { q := url.Values{} if closed != nil { q.Set("closed", strconv.Itoa(*closed)) } return c.Request(ctx, http.MethodGet, "/api/v2/alarms/list", q, nil) } func (c *Client) EventICS(ctx context.Context) (Response, error) { return c.Request(ctx, http.MethodGet, "/api/v2/events/ics", nil, nil) } func (c *Client) PullAll(ctx context.Context, q url.Values) (Response, error) { return c.Request(ctx, http.MethodGet, "/api/v2/pull/all", q, nil) } func (c *Client) PullVehicleStatus(ctx context.Context) (Response, error) { return c.Request(ctx, http.MethodGet, "/api/v2/pull/vehicle-status", nil, nil) } // ListUserClusterRelations returns the users of a Divera247 unit. The returned // id is the User-Cluster-Relation id (UCR id), i.e. the identifier expected by // alarm recipient filters. clusterID is required for some PRO structures. func (c *Client) ListUserClusterRelations(ctx context.Context, clusterID *int64) (Response, error) { q := url.Values{} if clusterID != nil && *clusterID > 0 { q.Set("clusterId", strconv.FormatInt(*clusterID, 10)) } return c.Request(ctx, http.MethodGet, "/api/v3/user-cluster-relations", q, nil) } func (c *Client) AddAttachment(ctx context.Context, kind string, id int64, filename, title, description string, data []byte) (Response, error) { cfg := c.cfg() if cfg.DryRun { return Response{StatusCode: 200, ContentType: "application/json", Body: []byte(`{"dry_run":true}`)}, nil } key, err := config.Secret(cfg.AccessKey) if err != nil { return Response{}, err } cfg.AccessKey = key var buf bytes.Buffer mw := multipart.NewWriter(&buf) part, err := mw.CreateFormFile("Attachment[upload]", filename) if err != nil { return Response{}, err } if _, err := part.Write(data); err != nil { return Response{}, err } _ = mw.WriteField("Attachment[title]", title) _ = mw.WriteField("Attachment[description]", description) _ = mw.Close() u, _ := url.Parse(strings.TrimRight(cfg.BaseURL, "/") + fmt.Sprintf("/api/v2/%s/attachment/%d", kind, id)) q := u.Query() q.Set("accesskey", cfg.AccessKey) if cfg.UCR != 0 { q.Set("ucr", strconv.FormatInt(cfg.UCR, 10)) } u.RawQuery = q.Encode() req, err := http.NewRequestWithContext(ctx, http.MethodPost, u.String(), &buf) if err != nil { return Response{}, err } req.Header.Set("Content-Type", mw.FormDataContentType()) resp, err := (&http.Client{Timeout: time.Duration(cfg.TimeoutS) * time.Second, CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }}).Do(req) if err != nil { return Response{}, fmt.Errorf("Divera247 HTTP request failed (connection or timeout)") } defer resp.Body.Close() b, _ := io.ReadAll(io.LimitReader(resp.Body, 8<<20)) out := Response{StatusCode: resp.StatusCode, ContentType: resp.Header.Get("Content-Type"), Body: b} if resp.StatusCode < 200 || resp.StatusCode >= 300 { out.Body = nil return out, fmt.Errorf("Divera247 API HTTP %d", resp.StatusCode) } return out, nil }