209 lines
7.9 KiB
Go
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
|
|
}
|