Files
2026-09-16 06:26:16 +02:00

209 lines
7.9 KiB
Go

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
}