🏡 index : ~doyle/hkbi.git

package blueiris

import (
	"bytes"
	"crypto/md5"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"sync"
)

type BlueirisConfig struct {
	Instance string
	Username string
	Password string
}

type Blueiris struct {
	mutex        *sync.RWMutex
	sessionToken string
	BaseUrl      *url.URL
	username     string
	password     string
}

func NewBlueiris(config BlueirisConfig) (*Blueiris, error) {
	base, err := url.Parse(config.Instance)
	if err != nil {
		return nil, err
	}

	bi := &Blueiris{
		mutex:        &sync.RWMutex{},
		sessionToken: "",
		BaseUrl:      base,
		username:     config.Username,
		password:     config.Password,
	}

	err = bi.login()
	if err != nil {
		return nil, err
	}

	return bi, nil
}

func (b *Blueiris) getSessionToken() string {
	cmd := struct {
		Cmd string `json:"cmd"`
	}{
		Cmd: "login",
	}

	var result struct {
		Session string `json:"session"`
	}

	_ = b.sendRequest(cmd, &result)

	return result.Session
}

func (b *Blueiris) login() error {
	b.mutex.Lock()
	defer b.mutex.Unlock()

	b.sessionToken = b.getSessionToken()

	tokenHash := md5.Sum([]byte(fmt.Sprintf("%s:%s:%s", b.username, b.sessionToken, b.password)))
	token := hex.EncodeToString(tokenHash[:])

	cmd := struct {
		Cmd      string `json:"cmd"`
		Session  string `json:"session"`
		Response string `json:"response"`
	}{
		Cmd:      "login",
		Session:  b.sessionToken,
		Response: token,
	}

	var result struct {
		Result string `json:"result"`
	}

	err := b.sendRequest(cmd, &result)
	if err != nil {
		return err
	} else if result.Result != "success" {
		return errors.New("unsuccessful login")
	}

	return nil
}

type Camera struct {
	Id       string `json:"optionValue"`
	Name     string `json:"optionDisplay"`
	IsOnline bool   `json:"isOnline"`
	HasAudio bool   `json:"audio"`
	IsGroup  bool   `json:"group"`
	IsSystem bool   `json:"is_system"`
	Type     int    `json:"type"`
}

func (b *Blueiris) ListCameras() ([]Camera, error) {
	b.mutex.RLock()
	defer b.mutex.RUnlock()

	request := struct {
		Cmd     string `json:"cmd"`
		Session string `json:"session"`
	}{
		Cmd:     "camlist",
		Session: b.sessionToken,
	}

	var response struct {
		Data []Camera `json:"data"`
	}

	err := b.sendRequest(request, &response)
	if err != nil {
		return nil, err
	}

	return response.Data, nil
}

//lint:ignore U1000 usage not yet implemented
func (b *Blueiris) triggerCamera(camera string) error {
	b.mutex.RLock()
	defer b.mutex.RUnlock()

	request := struct {
		Cmd     string `json:"cmd"`
		Camera  string `json:"camera"`
		Session string `json:"session"`
	}{
		Cmd:     "trigger",
		Camera:  camera,
		Session: b.sessionToken,
	}

	response := struct{}{}

	err := b.sendRequest(request, &response)
	if err != nil {
		return err
	}

	return nil
}

func (b *Blueiris) FetchSnapshot(camera string) (*http.Request, error) {
	uri := b.BaseUrl.JoinPath("image", camera)
	uri.User = url.UserPassword(b.username, b.password)
	return http.NewRequest("GET", uri.String(), nil)
}

// TODO: try to renew session if result == 'fail'
func (b *Blueiris) sendRequest(request interface{}, res any) error {
	buf, err := json.Marshal(request)
	if err != nil {
		return err
	}

	reader := bytes.NewReader(buf)

	uri := b.BaseUrl.JoinPath("json")
	response, err := http.Post(uri.String(), "application/json", reader)
	if err != nil {
		return err
	}

	responseBytes, err := io.ReadAll(response.Body)
	if err != nil {
		return err
	}

	return json.Unmarshal(responseBytes, res)
}