Files
go-meater/meater.go
2025-02-03 16:30:36 -05:00

283 lines
6.4 KiB
Go

package meater
import (
"encoding/json"
"errors"
"os"
"github.com/sirupsen/logrus"
"github.com/go-resty/resty/v2"
)
var logger = logrus.New()
// credentials .
type credentials struct {
username string
password string
}
// Client .
type Client struct {
baseURL string
credentials credentials
httpClient *resty.Client
updateInterval int
fetchInterval int
isAuthenticated bool
logLevel string
}
// Response .
type Response struct {
Status string `json:"status"`
StatusCode int `json:"statusCode"`
Data json.RawMessage `json:"data,omitempty"`
Meta *json.RawMessage `json:"meta,omitempty"`
Message *string `json:"message,omitempty"`
Help *string `json:"help,omitempty"`
}
// Auth .
type Auth struct {
Token string `json:"token"`
UserId string `json:"userId"`
}
// Devices .
type Devices struct {
Probes []*Probe `json:"devices,omitempty"`
}
// Option .
type Option func(*Client) error
// BaseURL allows overriding of API client baseURL for testing
func BaseURL(baseURL string) Option {
return func(c *Client) error {
c.baseURL = baseURL
return nil
}
}
// Username .
func Username(username string) Option {
return func(c *Client) error {
c.credentials.username = username
return nil
}
}
// Password .
func Password(password string) Option {
return func(c *Client) error {
c.credentials.password = password
return nil
}
}
// LogLevel allows overriding of API client baseURL for testing
func LogLevel(logLevel string) Option {
return func(c *Client) error {
c.logLevel = logLevel
return nil
}
}
// auth .
func (c *Client) auth() bool {
params := map[string]string{
"email": c.credentials.username,
"password": c.credentials.password,
}
reqURL := API_VERSION + apiURLs["API_LOGIN"]
resp, err := c.execute(reqURL, POST, params, true)
if err != nil {
return false
}
auth := Auth{}
json.Unmarshal([]byte(resp), &auth)
c.httpClient.SetAuthToken(auth.Token)
return true
}
// parseOptions parses the supplied options functions and returns a configured
// *Client instance
func (c *Client) parseOptions(opts ...Option) error {
// Range over each options function and apply it to our API type to
// configure it. Options functions are applied in order, with any
// conflicting options overriding earlier calls.
for _, option := range opts {
err := option(c)
if err != nil {
return err
}
}
return nil
}
// New function creates a New Meater client
func New(opts ...Option) (*Client, error) {
client := Client{
updateInterval: 7200,
fetchInterval: 360,
}
if err := client.parseOptions(opts...); err != nil {
return nil, err
}
logger.SetOutput(os.Stdout)
logger.SetFormatter(&logrus.TextFormatter{}) // &log.TextFormatter{} | &log.JSONFormatter{}
switch client.logLevel {
case "debug":
logger.SetLevel(logrus.DebugLevel)
case "info":
logger.SetLevel(logrus.InfoLevel)
case "warn":
logger.SetLevel(logrus.WarnLevel)
case "error":
logger.SetLevel(logrus.ErrorLevel)
default:
logger.SetLevel(logrus.DebugLevel)
logger.Warnf("[MEATER] Invalid log level supplied: '%s'", logger.GetLevel())
}
httpClient := resty.New()
if client.baseURL != "" {
httpClient.SetBaseURL(client.baseURL)
} else {
httpClient.SetBaseURL(API_SERVER)
}
httpClient.
SetHeaders(map[string]string{
"Accept": "application/json",
"User-Agent": "Mozilla/5.0 (Linux; Android 10; Android SDK built for x86 Build/QSR1.191030.002; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/74.0.3729.185 Mobile Safari/537.36",
"Accept-Language": "en-US,en;q=0.9",
"Accept-Encoding": "gzip, deflate"})
client.httpClient = httpClient
if client.auth() {
return &client, nil
}
return &client, nil
}
// ProbesList .
func (c *Client) GetProbes() []*Probe {
reqURL := API_VERSION + apiURLs["API_DEVICES"]
resp, err := c.execute(reqURL, GET, map[string]string{}, false)
if err != nil {
panic(err)
}
logger.Debugf("[MEATER] PROBES: %+v", string(resp))
devices := Devices{}
err = json.Unmarshal(resp, &devices)
if err != nil {
panic(err)
}
if len(devices.Probes) > 0 {
for _, p := range devices.Probes {
p.client = c
}
}
return devices.Probes
}
// GetProbeByID .
func (c *Client) GetProbeByID(pID string) *Probe {
reqURL := API_VERSION + apiURLs["API_DEVICES"] + "/" + pID
resp, err := c.execute(reqURL, GET, map[string]string{}, false)
if err != nil {
panic(err)
}
logger.Debugf("[MEATER] PROBE: %+v", string(resp))
probe := Probe{client: c}
err = json.Unmarshal(resp, &probe)
if err != nil {
panic(err)
}
return &probe
}
// Exec method executes a Client instance with the API URL
// Rate limit:
//
// Recommended: 2 requests per 60 seconds.
// Maximum: 60 requests per 60 seconds.
func (c *Client) execute(requestUrl string, method string, params map[string]string, jsonEnc bool) ([]byte, error) {
defer timeTrack("[MEATER][TIMETRK] Executing HTTP Request", logger)
var resp *resty.Response
// GET Requests
if method == "GET" {
resp, _ = c.httpClient.
R().
SetQueryParams(params).
Get(requestUrl)
}
// POST Requests
if method == "POST" {
if jsonEnc {
// POST > JSON Body
resp, _ = c.httpClient.R().
SetBody(params).
Post(requestUrl)
} else {
// POST > Form Data
resp, _ = c.httpClient.R().
SetFormData(params).
Post(requestUrl)
}
}
logger.Debugf("[MEATER] HTTP OUTPUT >> %v\n", string([]byte(resp.Body())))
respParsed := Response{}
json.Unmarshal([]byte(resp.Body()), &respParsed)
if respParsed.StatusCode == 200 {
c.isAuthenticated = true
return respParsed.Data, nil
} else {
c.isAuthenticated = false
switch error := respParsed.Status; error {
case apiErrors["API_ERROR_NOT_FOUND"]:
logger.Debugf("[MEATER] Not Found")
return nil, errors.New("not found")
case apiErrors["API_ERROR_UNAUTHORIZED"]:
logger.Debugf("[MEATER] Client authentication failed")
return nil, errors.New("client authentication failed")
case apiErrors["API_ERROR_BAD_REQUEST"]:
logger.Debugf("[MEATER] Bad Request")
return nil, errors.New("bad request")
case apiErrors["API_ERROR_TOO_MANY_REQUESTS"]:
logger.Debugf("[MEATER] Too Many Requests")
return nil, errors.New("too many requests")
case apiErrors["API_ERROR_INTERNAL_SERVER_ERROR"]:
logger.Debugf("[MEATER] Internal Server Error")
return nil, errors.New("internal server error")
default:
logger.Debugf("[MEATER] Uknown error")
return nil, errors.New("uknown error")
}
}
}